Search code examples
swiftgoogle-maps-sdk-ios

How to display large amount of GMSPolylines without maxing out CPU usage?


I'm making an app that displays bus routes using the NextBus API and Google Maps. However, I'm having an issue with CPU usage that I think is being caused by the amount of GMSPolylines on the map. The route is displayed by an array of polylines made up of the points given by NextBus for a given route. When the polylines are added to the map and the GMSCamera is overviewing the entire route, the CPU on the simulator (iPhone X) maxes out at 100%. When zoomed in on a particular section of the route, however, the CPU usage goes down to ~2%.

Map Screenshot: https://i.sstatic.net/vFQo9.png Performance: https://i.sstatic.net/irIwb.png

The NextBus API returns route information including the route of a specific bus path. Here's an small example of the data that I'm working with:

Route: {
    "path": [Path]
}

Path: {
    "points:" [Coordinate]
}

Coordinate: {
    "lat": Float,
    "lon": Float
}

And here's my method that creates the polylines from the data. All in all there are on average ~700 coordinates spread across ~28 polylines (each path object) for a route. Keep in mind I'm not displaying multiple routes on one page, I'm only displaying one at a time.

func buildRoute(routePath: [Path?]) -> [GMSPolyline] {
    var polylines: [GMSPolyline] = []

    for path in routePath {
         let path = GMSMutablePath()
         guard let coords = path?.points else {continue}

         for coordinate in coords {
            // Safely unwrap latitude strings and convert them to doubles.
            guard let latStr = coordinate?.lat,
                  let lonStr = coordinate?.lon else {
                      continue
            }

            guard let latOne = Double(latStr),
                  let lonOne = Double(lonStr) else {
                      continue
            }

            // Create location coordinates.
            let pointCoordinatie = CLLocationCoordinate2D(latitude: latOne, longitude: lonOne)
            path.add(pointCoordinatie)
        }

        let line = GMSPolyline(path: path)
        line.strokeWidth = 6
        line.strokeColor = UIColor(red: 0/255, green: 104/255, blue: 139/255, alpha: 1.0)
        polylines.append(line)
    }

    return polylines
}

Finally here is my method that adds the polylines to the map:

fileprivate func buildRoute(routeConfig: RouteConfig?) {
    if let points = routeConfig?.route?.path {
        let polylines = RouteBuiler.shared.buildRoute(routePath: points)

        DispatchQueue.main.async {
            // Remove polylines from map if there are any.
            for line in self.currentRoute {
                line.map = nil
            }

            // Set new current route and add it to the map.
            self.currentRoute = polylines
            for line in self.currentRoute {
                line.map = self.mapView
            }
        }
    }
}

Is there a problem with how I'm constructing the polylines? Or are there simply too many coordinates?


Solution

  • I ran into this exact problem. It is quite an odd bug -- when you go over a certain threshold of polylines, the CPU suddenly pegs to 100%.

    I discovered that GMSPolygon does not have this problem. So I switched over all of GMSPolyline to GMSPolygon.

    To get the correct stroke width, I am using the following code to create a polygon that traces the outline of a polyline at a given stroke width. My calculation requires the LASwift linear algebra library.

    https://github.com/AlexanderTar/LASwift

    import CoreLocation
    import LASwift
    import GoogleMaps
    
    struct Segment {
        let from: CLLocationCoordinate2D
        let to: CLLocationCoordinate2D
    }
    
    enum RightLeft {
        case right, left
    }
    
    // Offset the given path to the left or right by the given distance
    func offsetPath(rightLeft: RightLeft, path: [CLLocationCoordinate2D], offset: Double) -> [CLLocationCoordinate2D] {
        var offsetPoints = [CLLocationCoordinate2D]()
        var prevSegment: Segment!
    
        for i in 0..<path.count {
            // Test if this is the last point
            if i == path.count-1 {
                if let to = prevSegment?.to {
                    offsetPoints.append(to)
                }
                continue
            }
    
            let from = path[i]
            let to = path[i+1]
    
            // Skip duplicate points
            if from.latitude == to.latitude && from.longitude == to.longitude {
                continue
            }
    
            // Calculate the miter corner for the offset point
            let segmentAngle = -atan2(to.latitude - from.latitude, to.longitude - from.longitude)
            let sinA = sin(segmentAngle)
            let cosA = cos(segmentAngle)
            let rotate =
                Matrix([[cosA, -sinA, 0.0],
                        [sinA, cosA, 0.0],
                        [0.0, 0.0, 1.0]])
            let translate =
                Matrix([[1.0, 0.0, 0.0 ],
                        [0.0, 1.0, rightLeft == .left ? offset : -offset ],
                        [0.0, 0.0, 1.0]])
            let mat = inv(rotate) * translate * rotate
    
            let fromOff = mat * Matrix([[from.x], [from.y], [1.0]])
            let toOff = mat * Matrix([[to.x], [to.y], [1.0]])
    
            let offsetSegment = Segment(
                from: CLLocationCoordinate2D(latitude: fromOff[1,0], longitude: fromOff[0,0]),
                to: CLLocationCoordinate2D(latitude: toOff[1,0], longitude: toOff[0,0]))
    
            if prevSegment == nil {
                prevSegment = offsetSegment
                offsetPoints.append(offsetSegment.from)
                continue
            }
    
            // Calculate line intersection
            guard let intersection = getLineIntersection(line0: prevSegment, line1: offsetSegment, segment: false) else {
                prevSegment = offsetSegment
                continue
            }
    
            prevSegment = offsetSegment
            offsetPoints.append(intersection)
        }
    
        return offsetPoints
    }
    
    // Returns the intersection point if the line segments intersect, otherwise nil
    func getLineIntersection(line0: Segment, line1: Segment, segment: Bool) -> CLLocationCoordinate2D? {
        return getLineIntersection(p0: line0.from, p1: line0.to, p2: line1.from, p3: line1.to, segment: segment)
    }
    
    // https://stackoverflow.com/questions/563198/how-do-you-detect-where-two-line-segments-intersect
    // Returns the intersection point if the line segments intersect, otherwise nil
    func getLineIntersection(p0: CLLocationCoordinate2D, p1: CLLocationCoordinate2D, p2: CLLocationCoordinate2D, p3: CLLocationCoordinate2D, segment: Bool) -> CLLocationCoordinate2D? {
        let s1x = p1.longitude - p0.longitude
        let s1y = p1.latitude - p0.latitude
        let s2x = p3.longitude - p2.longitude
        let s2y = p3.latitude - p2.latitude
    
        let numerator = (s2x * (p0.latitude - p2.latitude) - s2y * (p0.longitude - p2.longitude))
        let denominator = (s1x * s2y - s2x * s1y)
        if denominator == 0.0 {
            return nil
        }
        let t =  numerator / denominator
    
        if segment {
            let s = (s1y * (p0.longitude - p2.longitude) + s1x * (p0.latitude - p2.latitude)) / (s1x * s2y - s2x * s1y)
            guard (s >= 0 && s <= 1 && t >= 0 && t <= 1) else {
                return nil
            }
        }
    
        return CLLocationCoordinate2D(latitude: p0.latitude + (t  * s1y), longitude: p0.longitude + (t * s1x))
    }
    
    
    // The path from NextBus
    let path: CLLocationCoordinate2D = pathFromNextBus()
    
    // The desired width of the polyline
    let strokeWidth: Double = desiredPolylineWidth()
    
    let polygon: GMSPolygon
    do {
        let polygonPath = GMSMutablePath()
        let w = strokeWidth / 2.0
        for point in offsetPath(rightLeft: .left, path: route.offsetPath, offset: w) {
            polygonPath.add(CLLocationCoordinate2D(latitude: point.latitude, longitude: point.longitude))
        }
        for point in offsetPath(rightLeft: .right, path: route.offsetPath, offset: w).reversed() {
            polygonPath.add(CLLocationCoordinate2D(latitude: point.latitude, longitude: point.longitude))
        }
        polygon = GMSPolygon(path: polygonPath)
        polygon.strokeWidth = 0.0
    }