Swift attach UIView to PolyLine and follow angle - ios

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
}

Related

can't point to right location in compass Swift ios

I am developing a compass that can point from my current location to other location, but after implementing it, I noticed that it always points to the same direction every time of choosing a new target location to go or to point.
func locationManager(_ manager: CLLocationManager, didUpdateHeading newHeading: CLHeading) {
let heading = newHeading.magneticHeading
let headingR = Measurement(value: newHeading.trueHeading, unit: UnitAngle.degrees).converted(to: .radians).value
// To know if the Data that our device is correct, The "horizontalAccuracy" value should be grater then ZERO for valid GPS coordinate Data
if heading < 0 {return}
// Get the heading(direction)
if(heading == -1){
print("no location")
}
self.directionAngel = Double(self.getBearingBetweenTwoPoints1(latDestination: self.desiredLatit,lonDestination: self.desiredLongit))
let north = -1 * heading.degreesToRadians
UIView.animate(withDuration: 1) {
self.imageView.transform = CGAffineTransform(rotationAngle: CGFloat(north))
self.directionArrowView.transform = CGAffineTransform(rotationAngle: CGFloat(self.directionAngel.degreesToRadians - headingR))
}
self.updateHeading(heading: heading)
}
private func getBearingBetweenTwoPoints1(latDestination:Double,lonDestination:Double) -> Double {
let lat1 = getMyLocation().latitude.degreesToRadians
let lon1 = getMyLocation().longitude.degreesToRadians
let lat2 = latDestination.degreesToRadians
let lon2 = lonDestination.degreesToRadians
let dLon = lon2 - lon1
let y = sin(dLon) * cos(lat2)
let x = cos(lat1) * sin(lat2) - sin(lat1) * cos(lat2) * cos(dLon)
var radiansBearing = atan2(y, x)
if(radiansBearing < 0.0){
radiansBearing += 2 * Double.pi
}
return radiansBearing
}
let locationManager: CLLocationManager = {
$0.requestAlwaysAuthorization()
$0.requestWhenInUseAuthorization()
$0.desiredAccuracy = kCLLocationAccuracyNearestTenMeters
$0.startUpdatingLocation()
$0.startUpdatingHeading()
return $0
}(CLLocationManager())

How to draw an arc on Google Maps in iOS?

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
}

Calculate new coordinates with starting position and distance

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
}

Given two sets of coordinates, how do I calculate the distance between them?

Suppose I have two points:
var lat1 = 37.7833
var lon1 = -122.4167
var lat2 = 39.4811
var lon2 = -118.9558
How can I calculate the distance between them, the simplest way? (taking into account the curvature of the earth)
I looked into this answer, but CCLocation doesn't have an initializer in Swift 2.0
let coord1 = CLLocation(latitude: 37.7833, longitude: -122.4167)
let coord2 = CLLocation(latitude: 39.4811, longitude: -118.9558)
let distance = coord1.distance(from: coord2)
print("the distance between the two points is: \(distance) meters")
I have been using this and works perfectly for me :-
static let DEG_TO_RAD = 0.017453292519943295769236907684886
static let EARTH_RADIUS_IN_METERS = 6372797.560856
static func calculateDistance(fromlat : Double, fromlon : Double, tolat : Double, tolon : Double) -> Double {
let latitudeArc : Double = (fromlat - tolat) * DEG_TO_RAD
let longitudeArc : Double = (fromlon - tolon) * DEG_TO_RAD
var latitudeH : Double = sin(latitudeArc * 0.5)
latitudeH *= latitudeH
var lontitudeH : Double = sin(longitudeArc * 0.5)
lontitudeH *= lontitudeH
let tmp : Double = cos(fromlat*DEG_TO_RAD) * cos(tolat*DEG_TO_RAD)
return EARTH_RADIUS_IN_METERS * 2.0 * asin(sqrt(latitudeH + tmp*lontitudeH))
}

Display lateral view same like google map in my application

I am developing one navigation based application in which i used google map and google direction api for route navigation but i stuck at one point, i need to show same google map lateral view which is open when we start the navigation.
I have R&D lots off on it but not able to do exactly same like google map lateral view.
I try with following
GMSCameraPosition *camera =[GMSCameraPosition cameraWithTarget:marker.position zoom:50.0 bearing:120 viewingAngle:90];
[[self getMapView] animateToCameraPosition:camera];
not succeed to display same.
I need to show same google map as following when i start navigation.Can anybody help me in this point.Thanks in advance
You can achieve using proper bearing between 2 location. (present and target location)
func degreesToRadians(degrees: Double) -> Double { return degrees * .pi / 180.0 }
func radiansToDegrees(radians: Double) -> Double { return radians * 180.0 / .pi }
func getBearingBetweenTwoPoints1(point1 : CLLocation, point2 : CLLocation) -> Double {
let lat1 = degreesToRadians(degrees: point1.coordinate.latitude)
let lon1 = degreesToRadians(degrees: point1.coordinate.longitude)
let lat2 = degreesToRadians(degrees: point2.coordinate.latitude)
let lon2 = degreesToRadians(degrees: point2.coordinate.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(radians: radiansBearing)
}
let bearingPoint = self.getBearingBetweenTwoPoints1(point1: location!, point2: self.featureLocation)
let camera = GMSCameraPosition.camera(withLatitude: (location?.coordinate.latitude)!, longitude: (location?.coordinate.longitude)!, zoom: 50, bearing: bearingPoint, viewingAngle: 45)
self.mapView?.animate(to: camera)

Resources