jrend
jrend

Reputation: 1102

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?

Upvotes: 3

Views: 740

Answers (1)

dougzilla
dougzilla

Reputation: 166

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
}

Upvotes: 1

Related Questions