Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

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 ?

like image 929
Karthik Mandava Avatar asked Oct 25 '17 13:10

Karthik Mandava


4 Answers

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)
    }
}
like image 120
Rouny Avatar answered Oct 07 '22 09:10

Rouny


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)
  }
like image 45
chatterjee86 Avatar answered Oct 07 '22 08:10

chatterjee86


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)]
    }
}
like image 36
Jeffrey Liu Avatar answered Oct 07 '22 09:10

Jeffrey Liu


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
 }
like image 42
Md.Saber Hossain Avatar answered Oct 07 '22 09:10

Md.Saber Hossain