简体   繁体   中英

How to draw an arc on Google Maps in iOS?

如何在谷歌地图中的两个坐标点之间绘制一条弧线,就像这张图片和iOS中的facebook帖子一样?

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)
    }
}

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)
  }

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
 }

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
 }

在此处输入图片说明

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM