I'm attempting to have a UIView follow a Polyline using the angle of the Polyline. The problem I'm facing is the rotation is never right unless the angle is lower than 50 degrees, which I'm thinking is just a coincidence. How can I get the rotation to follow the line?
Here's the code I'm using to detect the rotation.
func heading(from: MSGeopoint, to: MSGeopoint) -> Double {
let a = CLLocationCoordinate2D(latitude: from.position.latitude, longitude: from.position.longitude)
let b = CLLocationCoordinate2D(latitude: to.position.latitude, longitude: to.position.longitude)
let lat1 = a.latitude.degreesToRadians
let lon1 = a.longitude.degreesToRadians
let lat2 = b.latitude.degreesToRadians
let lon2 = b.longitude.degreesToRadians
let dLon = lon2 - lon1
let y = sin(dLon) * cos(lat2)
let x = cos(lat1) * sin(lat2) - sin(lat1) * cos(lat2) * cos(dLon)
let headingDegrees = atan2(y, x).radiansToDegrees
if headingDegrees >= 0 {
print("\(headingDegrees)")
return headingDegrees
} else {
print("\(headingDegrees + 360)")
return headingDegrees + 360
}
}
The MSGeoPoint is primarily a wrapper for CoreLocation using Bing maps but here's how you instantiate the from and to.
let fromGeoPoint = MSGeopoint(latitude: from.latitude, longitude: from.longitude)
let toGeoPoint = MSGeopoint(latitude: to.latitude, longitude: to.longitude)
And the degreesToRadians.
extension BinaryInteger {
var degreesToRadians: CGFloat { return CGFloat(Int(self)) * .pi / 180 }
}
Here's also how I'm creating the view.
func createDistanceView() -> UIView {
let distanceView: DistanceView = UIView.fromNib()
distanceView.layer.borderColor = UIColor(named: "Blue")?.cgColor ?? UIColor.blue.cgColor
distanceView.layer.borderWidth = 3
distanceView.layer.masksToBounds = true
let heading = heading(from: from, to: to).rounded(.toNearestOrAwayFromZero)
distanceView.distanceLabel.text = "Your almost there"
return distanceView
}
I am integrating Mapbox iOS SDK into my app.
Right now I am stuck at a point where I want to achieve car tracking feature like Uber app.
I used to have that feature with Google Maps SDK but I cant make it work with Mapbox SDK.
I am adding MGLPointAnnotation object to add it on map and want to move it from point A to point B with animation.
I am doing it using
UIView.animate(withDuration: TimeInterval(duration), animations: {
// Update annotation coordinate to be the destination coordinate
point.coordinate = newCoordinate
}, completion: nil)
But for MGLPointAnnotation I can't change its image because when there's a turn I want to rotate the image(Annotation).
If I use MGLAnnotationView object I can change the image but I cant change its coordinate because its readonly.
What should I do here to achieve that functionality?
Several years ago I wrote smth similar, I used GoggleMaps, look through the code, maybe it will be helpful, I really don't remember what are all that numbers in angle counting
extension Double {
var degrees: Double {
return self * 180.0 / Double.pi
}
}
extension CLLocationCoordinate2D {
func angleToPosition(position : CLLocationCoordinate2D) -> CLLocationDegrees {
let bearingRadians = atan2(Double(position.latitude - latitude), Double(position.longitude - longitude))
var bearingDegrees = bearingRadians.degrees
// print("\(bearingDegrees)")
var roundDegrees = 360.0
if bearingDegrees < 0 {
if bearingDegrees > -90 {
roundDegrees = 350
}
if bearingDegrees < -90 && bearingDegrees > -180 {
roundDegrees = 370
}
bearingDegrees += roundDegrees
return 360 - bearingDegrees
}
roundDegrees = bearingDegrees < 90 ? 350 : 360
if bearingDegrees > 90 && bearingDegrees < 180 {
roundDegrees = 370
}
UserDefaults.standard.set(bearingDegrees, forKey: "bearingDegrees")
return roundDegrees - bearingDegrees
}
func duration(toDestination destination: CLLocationCoordinate2D, withSpeed speed : Double) -> Double {
let distance = GMSGeometryDistance(self, destination)
return distance / (speed * (1000 / 3600))
}
}
And this is the func which do the rotation, you can use it as soon as you receive new coordinates, or call it in for loop if you have certain polyline
func animateCarDrive(info: [String: Any]) {
let speed = info["speed"] as? Double ?? 40 // if your car's speed is changeable
let position = info["position"] as? CLLocationCoordinate2D // new point position
let duration = marker.position.duration(toDestination: position, withSpeed: speed)
marker.rotation = marker.position.angleToPosition(position: position)
CATransaction.begin()
CATransaction.setAnimationDuration(duration)
marker.position = position
CATransaction.commit()
}
I am creating an Uber like iOS app(swift).I have integrated google map and added marker.Also i am receiving current latitude and longitude of the vehicle from backend API. Now i want to show the movement of vehicle in my app. I have written some code and it is working. But there is some issue with movement of the vehicle. There is some difference between position of marker in my app and the actual location of vehicle. Accuracy is not good. Here i am sharing my code.I calls the backend API in each 5 seconds.
func moveCab()
{
let oldCoordinate: CLLocationCoordinate2D? = CLLocationCoordinate2DMake(Double(oldLatitude)!,Double(oldLongitude)!)
let newCoordinate: CLLocationCoordinate2D? = CLLocationCoordinate2DMake(Double(currentLatitude)!,Double(currentLongitude)!)
mMarker.groundAnchor = CGPoint(x: CGFloat(0.5), y: CGFloat(0.5))
mMarker.rotation = CLLocationDegrees(getHeadingForDirection(fromCoordinate: oldCoordinate!, toCoordinate: newCoordinate!))
//found bearing value by calculation when marker add
mMarker.position = oldCoordinate!
//this can be old position to make car movement to new position
mMarker.map = mapView
//marker movement animation
CATransaction.begin()
CATransaction.setValue(Int(2.0), forKey: kCATransactionAnimationDuration)
CATransaction.setCompletionBlock({() -> Void in
self.mMarker.groundAnchor = CGPoint(x: CGFloat(0.5), y: CGFloat(0.5))
// mMarker.rotation = CDouble(data.value(forKey: "bearing"))
//New bearing value from backend after car movement is done
})
mMarker.position = newCoordinate!
mMarker.map = mapView
mMarker.groundAnchor = CGPoint(x: CGFloat(0.5), y: CGFloat(0.5))
mMarker.rotation = CLLocationDegrees(getHeadingForDirection(fromCoordinate: oldCoordinate!, toCoordinate: newCoordinate!))
//found bearing value by calculation
CATransaction.commit()
oldLatitude = currentLatitude
oldLongitude = currentLongitude
}
func getHeadingForDirection(fromCoordinate fromLoc: CLLocationCoordinate2D, toCoordinate toLoc: CLLocationCoordinate2D) -> Float {
let fLat: Float = Float((fromLoc.latitude).degreesToRadians)
let fLng: Float = Float((fromLoc.longitude).degreesToRadians)
let tLat: Float = Float((toLoc.latitude).degreesToRadians)
let tLng: Float = Float((toLoc.longitude).degreesToRadians)
let degree: Float = (atan2(sin(tLng - fLng) * cos(tLat), cos(fLat) * sin(tLat) - sin(fLat) * cos(tLat) * cos(tLng - fLng))).radiansToDegrees
if degree >= 0 {
return degree
}
else {
return 360 + degree
}
extension Int {
var degreesToRadians: Double { return Double(self) * .pi / 180 }
}
extension FloatingPoint {
var degreesToRadians: Self { return self * .pi / 180 }
var radiansToDegrees: Self { return self * 180 / .pi }
}
How to draw an arc between two coordinate points in Google Maps like in this image and same like facebook post in iOS ?
Before using the below function, don't forget to import GoogleMaps
Credits: xomena
func drawArcPolyline(startLocation: CLLocationCoordinate2D?, endLocation: CLLocationCoordinate2D?) {
if let startLocation = startLocation, let endLocation = endLocation {
//swap the startLocation & endLocation if you want to reverse the direction of polyline arc formed.
let mapView = GMSMapView()
let path = GMSMutablePath()
path.add(startLocation)
path.add(endLocation)
// Curve Line
let k: Double = 0.2 //try between 0.5 to 0.2 for better results that suits you
let d = GMSGeometryDistance(startLocation, endLocation)
let h = GMSGeometryHeading(startLocation, endLocation)
//Midpoint position
let p = GMSGeometryOffset(startLocation, d * 0.5, h)
//Apply some mathematics to calculate position of the circle center
let x = (1 - k * k) * d * 0.5 / (2 * k)
let r = (1 + k * k) * d * 0.5 / (2 * k)
let c = GMSGeometryOffset(p, x, h + 90.0)
//Polyline options
//Calculate heading between circle center and two points
let h1 = GMSGeometryHeading(c, startLocation)
let h2 = GMSGeometryHeading(c, endLocation)
//Calculate positions of points on circle border and add them to polyline options
let numpoints = 100.0
let step = ((h2 - h1) / Double(numpoints))
for i in stride(from: 0.0, to: numpoints, by: 1) {
let pi = GMSGeometryOffset(c, r, h1 + i * step)
path.add(pi)
}
//Draw polyline
let polyline = GMSPolyline(path: path)
polyline.map = mapView // Assign GMSMapView as map
polyline.strokeWidth = 3.0
let styles = [GMSStrokeStyle.solidColor(UIColor.black), GMSStrokeStyle.solidColor(UIColor.clear)]
let lengths = [20, 20] // Play with this for dotted line
polyline.spans = GMSStyleSpans(polyline.path!, styles, lengths as [NSNumber], .rhumb)
let bounds = GMSCoordinateBounds(coordinate: startLocation, coordinate: endLocation)
let insets = UIEdgeInsets(top: 20, left: 20, bottom: 20, right: 20)
let camera = mapView.camera(for: bounds, insets: insets)!
mapView.animate(to: camera)
}
}
I used Bezier quadratic equation to draw curved lines. You can have a look on to the implementation. Here is the sample code.
func bezierPath(from startLocation: CLLocationCoordinate2D, to endLocation: CLLocationCoordinate2D) -> GMSMutablePath {
let distance = GMSGeometryDistance(startLocation, endLocation)
let midPoint = GMSGeometryInterpolate(startLocation, endLocation, 0.5)
let midToStartLocHeading = GMSGeometryHeading(midPoint, startLocation)
let controlPointAngle = 360.0 - (90.0 - midToStartLocHeading)
let controlPoint = GMSGeometryOffset(midPoint, distance / 2.0 , controlPointAngle)
let path = GMSMutablePath()
let stepper = 0.05
let range = stride(from: 0.0, through: 1.0, by: stepper)// t = [0,1]
func calculatePoint(when t: Double) -> CLLocationCoordinate2D {
let t1 = (1.0 - t)
let latitude = t1 * t1 * startLocation.latitude + 2 * t1 * t * controlPoint.latitude + t * t * endLocation.latitude
let longitude = t1 * t1 * startLocation.longitude + 2 * t1 * t * controlPoint.longitude + t * t * endLocation.longitude
let point = CLLocationCoordinate2D(latitude: latitude, longitude: longitude)
return point
}
range.map { calculatePoint(when: $0) }.forEach { path.add($0) }
return path
}
None of the answers mentioned is a full proof solution. For a few locations, it draws a circle instead of a polyline.
To resolve this we will calculate bearing(degrees clockwise from true north) and if it is less than zero, swap the start and end location.
func createArc(
startLocation: CLLocationCoordinate2D,
endLocation: CLLocationCoordinate2D) -> GMSPolyline {
var start = startLocation
var end = endLocation
if start.bearing(to: end) < 0.0 {
start = endLocation
end = startLocation
}
let angle = start.bearing(to: end) * Double.pi / 180.0
let k = abs(0.3 * sin(angle))
let path = GMSMutablePath()
let d = GMSGeometryDistance(start, end)
let h = GMSGeometryHeading(start, end)
let p = GMSGeometryOffset(start, d * 0.5, h)
let x = (1 - k * k) * d * 0.5 / (2 * k)
let r = (1 + k * k) * d * 0.5 / (2 * k)
let c = GMSGeometryOffset(p, x, h + 90.0)
var h1 = GMSGeometryHeading(c, start)
var h2 = GMSGeometryHeading(c, end)
if (h1 > 180) {
h1 = h1 - 360
}
if (h2 > 180) {
h2 = h2 - 360
}
let numpoints = 100.0
let step = ((h2 - h1) / Double(numpoints))
for i in stride(from: 0.0, to: numpoints, by: 1) {
let pi = GMSGeometryOffset(c, r, h1 + i * step)
path.add(pi)
}
path.add(end)
let polyline = GMSPolyline(path: path)
polyline.strokeWidth = 3.0
polyline.spans = GMSStyleSpans(
polyline.path!,
[GMSStrokeStyle.solidColor(UIColor(hex: "#2962ff"))],
[20, 20], .rhumb
)
return polyline
}
The bearing is the direction in which a vertical line on the map points, measured in degrees clockwise from north.
func bearing(to point: CLLocationCoordinate2D) -> Double {
func degreesToRadians(_ degrees: Double) -> Double { return degrees * Double.pi / 180.0 }
func radiansToDegrees(_ radians: Double) -> Double { return radians * 180.0 / Double.pi }
let lat1 = degreesToRadians(latitude)
let lon1 = degreesToRadians(longitude)
let lat2 = degreesToRadians(point.latitude);
let lon2 = degreesToRadians(point.longitude);
let dLon = lon2 - lon1;
let y = sin(dLon) * cos(lat2);
let x = cos(lat1) * sin(lat2) - sin(lat1) * cos(lat2) * cos(dLon);
let radiansBearing = atan2(y, x);
return radiansToDegrees(radiansBearing)
}
The answer above does not handle all the corner cases, here is one that draws the arcs nicely:
func drawArcPolyline(startLocation: CLLocationCoordinate2D?, endLocation: CLLocationCoordinate2D?) {
if let _ = startLocation, let _ = endLocation {
//swap the startLocation & endLocation if you want to reverse the direction of polyline arc formed.
var start = startLocation!
var end = endLocation!
var gradientColors = GMSStrokeStyle.gradient(
from: UIColor(red: 11.0/255, green: 211.0/255, blue: 200.0/255, alpha: 1),
to: UIColor(red: 0/255, green: 44.0/255, blue: 66.0/255, alpha: 1))
if startLocation!.heading(to: endLocation!) < 0.0 {
// need to reverse the start and end, and reverse the color
start = endLocation!
end = startLocation!
gradientColors = GMSStrokeStyle.gradient(
from: UIColor(red: 0/255, green: 44.0/255, blue: 66.0/255, alpha: 1),
to: UIColor(red: 11.0/255, green: 211.0/255, blue: 200.0/255, alpha: 1))
}
let path = GMSMutablePath()
// Curve Line
let k = abs(0.3 * sin((start.heading(to: end)).degreesToRadians)) // was 0.3
let d = GMSGeometryDistance(start, end)
let h = GMSGeometryHeading(start, end)
//Midpoint position
let p = GMSGeometryOffset(start, d * 0.5, h)
//Apply some mathematics to calculate position of the circle center
let x = (1-k*k)*d*0.5/(2*k);
let r = (1+k*k)*d*0.5/(2*k);
let c = GMSGeometryOffset(p, x, h + 90.0)
//Polyline options
//Calculate heading between circle center and two points
var h1 = GMSGeometryHeading(c, start)
var h2 = GMSGeometryHeading(c, end)
if(h1>180){
h1 = h1 - 360
}
if(h2>180){
h2 = h2 - 360
}
//Calculate positions of points on circle border and add them to polyline options
let numpoints = 100.0
let step = (h2 - h1) / numpoints
for i in stride(from: 0.0, to: numpoints, by: 1) {
let pi = GMSGeometryOffset(c, r, h1 + i * step)
path.add(pi)
}
path.add(end)
//Draw polyline
let polyline = GMSPolyline(path: path)
polyline.map = mapView // Assign GMSMapView as map
polyline.strokeWidth = 5.0
polyline.spans = [GMSStyleSpan(style: gradientColors)]
}
}
Objective-C version #Rouny answer
- (void)DrawCurvedPolylineOnMapFrom:(CLLocationCoordinate2D)startLocation To:(CLLocationCoordinate2D)endLocation
{
GMSMutablePath * path = [[GMSMutablePath alloc]init];
[path addCoordinate:startLocation];
[path addCoordinate:endLocation];
// Curve Line
double k = 0.2; //try between 0.5 to 0.2 for better results that suits you
CLLocationDistance d = GMSGeometryDistance(startLocation, endLocation);
float h = GMSGeometryHeading(startLocation , endLocation);
//Midpoint position
CLLocationCoordinate2D p = GMSGeometryOffset(startLocation, d * 0.5, h);
//Apply some mathematics to calculate position of the circle center
float x = (1-k*k)*d*0.5/(2*k);
float r = (1+k*k)*d*0.5/(2*k);
CLLocationCoordinate2D c = GMSGeometryOffset(p, x, h + -90.0);
//Polyline options
//Calculate heading between circle center and two points
float h1 = GMSGeometryHeading(c, startLocation);
float h2 = GMSGeometryHeading(c, endLocation);
//Calculate positions of points on circle border and add them to polyline options
float numpoints = 100;
float step = ((h2 - h1) / numpoints);
for (int i = 0; i < numpoints; i++) {
CLLocationCoordinate2D pi = GMSGeometryOffset(c, r, h1 + i * step);
[path addCoordinate:pi];
}
//Draw polyline
GMSPolyline * polyline = [GMSPolyline polylineWithPath:path];
polyline.map = mapView;
polyline.strokeWidth = 3.0;
NSArray *styles = #[[GMSStrokeStyle solidColor:kBaseColor],
[GMSStrokeStyle solidColor:[UIColor clearColor]]];
NSArray *lengths = #[#5, #5];
polyline.spans = GMSStyleSpans(polyline.path, styles, lengths, kGMSLengthRhumb);
GMSCoordinateBounds * bounds = [[GMSCoordinateBounds alloc]initWithCoordinate:startLocation coordinate:endLocation];
UIEdgeInsets insets = UIEdgeInsetsMake(20, 20, 20, 20);
GMSCameraPosition * camera = [mapView cameraForBounds:bounds insets:insets ];
[mapView animateToCameraPosition:camera];
}
Swift 5+
Very easy and Smooth way
//MARK: - Usage
let path = self.bezierPath(from: CLLocationCoordinate2D(latitude: kLatitude, longitude: kLongtitude), to: CLLocationCoordinate2D(latitude: self.restaurantLat, longitude: self.restaurantLong))
let polyline = GMSPolyline(path: path)
polyline.strokeWidth = 5.0
polyline.strokeColor = appClr
polyline.map = self.googleMapView // Google MapView
Simple Function
func drawArcPolyline(from startLocation: CLLocationCoordinate2D, to endLocation: CLLocationCoordinate2D) -> GMSMutablePath {
let distance = GMSGeometryDistance(startLocation, endLocation)
let midPoint = GMSGeometryInterpolate(startLocation, endLocation, 0.5)
let midToStartLocHeading = GMSGeometryHeading(midPoint, startLocation)
let controlPointAngle = 360.0 - (90.0 - midToStartLocHeading)
let controlPoint = GMSGeometryOffset(midPoint, distance / 2.0 , controlPointAngle)
let path = GMSMutablePath()
let stepper = 0.05
let range = stride(from: 0.0, through: 1.0, by: stepper)// t = [0,1]
func calculatePoint(when t: Double) -> CLLocationCoordinate2D {
let t1 = (1.0 - t)
let latitude = t1 * t1 * startLocation.latitude + 2 * t1 * t * controlPoint.latitude + t * t * endLocation.latitude
let longitude = t1 * t1 * startLocation.longitude + 2 * t1 * t * controlPoint.longitude + t * t * endLocation.longitude
let point = CLLocationCoordinate2D(latitude: latitude, longitude: longitude)
return point
}
range.map { calculatePoint(when: $0) }.forEach { path.add($0) }
return path
}
I am trying to get the coordinates of a point, that is on a set distance from a starting position, but the end result is wrong.
First, I calculate the angle between the starting position and the desired destination:
private func calculateAngleBetweenLocations(currentLocation: CLLocationCoordinate2D, targetLocation: CLLocationCoordinate2D) -> Double {
let fLat = self.degreesToRadians(currentLocation.latitude);
let fLng = self.degreesToRadians(currentLocation.longitude);
let tLat = self.degreesToRadians(targetLocation.latitude);
let tLng = self.degreesToRadians(targetLocation.longitude);
let deltaLng = tLng - fLng
let y = sin(deltaLng) * cos(tLat)
let x = cos(fLat) * sin(tLat) - sin(fLat) * cos(tLat) * cos(deltaLng)
let bearing = atan2(y, x)
return self.radiansToDegrees(bearing)
}
Then, I calculate the new point, given a distance:
private func coordinatesForMovement(endLocation: CLLocationCoordinate2D, distance: Double) -> CLLocationCoordinate2D {
let angle = self.calculateAngleBetweenLocations(self.currentLocation, targetLocation: endLocation)
let x = self.currentLocation.latitude + distance * cos(angle)
let y = self.currentLocation.longitude + distance * sin(angle)
return CLLocationCoordinate2D(latitude: x, longitude: y)
}
And this is the result (the feet are the starting position, the blue marker is the destination and the red marker is where the new calculated point is). I've tried passing the distance in meters and kilometers and every other floating point position, but never got the correct result. Any ideas?
Ok, after some more digging, I found this answer, which resolves my problem. Here is my complete solution in swift:
internal func moveToLocation(location: CLLocationCoordinate2D, distance: Double) {
let angle = self.calculateAngleBetweenLocations(self.currentLocation, targetLocation: location)
let newLocation = self.coordinates(self.currentLocation, atDistance: distance, atAngle: angle)
self.moveUser(newLocation: newLocation)
}
private func coordinates(startingCoordinates: CLLocationCoordinate2D, atDistance: Double, atAngle: Double) -> CLLocationCoordinate2D {
let distanceRadians = atDistance / 6371
let bearingRadians = self.degreesToRadians(atAngle)
let fromLatRadians = self.degreesToRadians(startingCoordinates.latitude)
let fromLonRadians = self.degreesToRadians(startingCoordinates.longitude)
let toLatRadians = asin(sin(fromLatRadians) * cos(distanceRadians) + cos(fromLatRadians) * sin(distanceRadians) * cos(bearingRadians))
var toLonRadians = fromLonRadians + atan2(sin(bearingRadians) * sin(distanceRadians) * cos(fromLatRadians), cos(distanceRadians) - sin(fromLatRadians) * sin(toLatRadians));
toLonRadians = fmod((toLonRadians + 3 * M_PI), (2 * M_PI)) - M_PI
let lat = self.radiansToDegrees(toLatRadians)
let lon = self.radiansToDegrees(toLonRadians)
return CLLocationCoordinate2D(latitude: lat, longitude: lon)
}
private func calculateAngleBetweenLocations(currentLocation: CLLocationCoordinate2D, targetLocation: CLLocationCoordinate2D) -> Double {
let fLat = self.degreesToRadians(currentLocation.latitude);
let fLng = self.degreesToRadians(currentLocation.longitude);
let tLat = self.degreesToRadians(targetLocation.latitude);
let tLng = self.degreesToRadians(targetLocation.longitude);
let deltaLng = tLng - fLng
let y = sin(deltaLng) * cos(tLat)
let x = cos(fLat) * sin(tLat) - sin(fLat) * cos(tLat) * cos(deltaLng)
let bearing = atan2(y, x)
return self.radiansToDegrees(bearing)
}
private func degreesToRadians(x: Double) -> Double {
return M_PI * x / 180.0
}
private func radiansToDegrees(x: Double) -> Double {
return x * 180.0 / M_PI
}