3

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.stack.imgur.com/vFQo9.png Performance: https://i.stack.imgur.com/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?

jrend
  • 784
  • 2
  • 11
  • 30
  • What are `RouteConfig` and `RouteBuiler` and where do they come from? – El Tomato Apr 07 '19 at 01:32
  • `RouteConfig` is just the object that is fetched from the NextBus API; it contains all of the information about a route, including the path. `RouteBuilder` is the class that contains the `buildRoute` method that generates the polylines from the `routeConfig.route.path` object. It's in a separate class because I need to reuse the code in other parts of the app. – jrend Apr 07 '19 at 01:35

1 Answers1

1

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
}
dougzilla
  • 156
  • 1
  • 5