1

I have a function that takes a [CLLocation] as an input. Inside a while loop it splits it into chunks, for each chunk makes a MKDirection request, store the response in a new [CLLocation] and returns it once completed.

The problem is that all the chunks in the new array are not sequential, so the resulting route jumps all over the place. Ho do I wait for the previous request to get a response before making a new one? I tried DispatchQueue.global().sync and DispatchQueue.main.sync but it doesn't make a difference. I tried to implement first answer from Cannot wait for the result of MKDirections.calculate, getting nil instead of it that seemed my same problem, but I'm not understanding how to adapt it to my case. Can you please help me to get the responses sequentially? This is the function, the commented out part is for the lates bit of the route, and that will be the the last request. As always many thanks for your help and time.

    func repositionLocation2(route: [CLLocation], completion: @escaping ([CLLocation]) -> Void) {
        let group = DispatchGroup()
        var pos = 0
        var nextPos = 3
        var repositioned = [CLLocation]()
        //        repositioned.append(route.first!)

        guard route.count > nextPos else {print("Reposision Location failed, not enough positions");return}
        let request = MKDirections.Request()
        request.requestsAlternateRoutes = false
        request.transportType = .walking

        while pos < route.count - nextPos {
            print(" pos in \(pos)")
            //            repositioned.removeAll()

            group.enter()
            // get a small chunk of the input route
            let a = route[pos].coordinate//repositioned.last!.coordinate//
            let b = route[pos + nextPos].coordinate


            // get directions for the small chunk
            request.source = MKMapItem(placemark: MKPlacemark(coordinate: a))
            request.destination = MKMapItem(placemark: MKPlacemark(coordinate: b))
            let directions = MKDirections(request: request)
//            DispatchQueue.main.sync {
//            DispatchQueue.global().sync {
//                group.enter()
                directions.calculate { [unowned self] response, error in
                    if let err = error {
                        print("direction error : \(err)")
                    }
                    guard let unwrappedResponse = response else {print("no suggested routes available"); return }
                    print("Response is: \(unwrappedResponse.debugDescription)")
                    guard let coord = unwrappedResponse.routes.first?.steps else {print("No coordinates");return}
                    print("coord is: \(coord)")
                    // save response coordinates into a new array
                    for location in coord {
                        let point: CLLocation = CLLocation(latitude: location.polyline.coordinate.latitude, longitude: location.polyline.coordinate.longitude)
                        print("point is: \(point)") // prints a correct CLLocation with coordinates
                        repositioned.append(point)
                        print("repositioned in for loop is : \(repositioned)") // prints just first appended location CLLocation with coordinates
//                        group.leave() 
                    }
//                    group.wait() // hangs the app
                    completion(repositioned)
                }
//            }
            print("repositioned in while loop is : \(repositioned)")
            // shift to nex addiacent chunk
            pos += 3
            nextPos += 3
        }

        //        // last chunk
        //        let a = route[pos - 5].coordinate//repositioned.last!.coordinate
        //        let b = route.last?.coordinate
        //        request.source = MKMapItem(placemark: MKPlacemark(coordinate: a))
        //        request.destination = MKMapItem(placemark: MKPlacemark(coordinate: b!))
        //        let directions = MKDirections(request: request)
        //        directions.calculate { [unowned self] response, error in
        //            if let err = error {
        //                print("direction error : \(err)")
        //            }
        //            guard let unwrappedResponse = response else {print("no suggested routes available"); return }
        //            print("Response is: \(unwrappedResponse.debugDescription)")
        //            guard let coord = unwrappedResponse.routes.first?.steps else {print("No coordinates");return}
        //            print("coord is: \(coord)")
        //            for location in coord {
        //
        //                let point: CLLocation = CLLocation(latitude: location.polyline.coordinate.latitude, longitude: location.polyline.coordinate.longitude)
        //                print("point is: \(point)")
        //                repositioned.append(point)
        //                print("repositioned in for loop is : \(repositioned)")
        //            }
        //            completion(repositioned)
        //        }
        //        print("repositioned in while loop is : \(repositioned)")

    }
Vincenzo
  • 5,304
  • 5
  • 38
  • 96

1 Answers1

2

When you have a series of asynchronous tasks (which may finish in any arbitrary order) where you want the results in order, just save it into a structure for which order doesn’t matter, just sorting it at the end. E.g., you could use a dictionary indexed by the integer index:

var routes: [Int: [CLLocationCoordinate2D]] = [:]

Then when any given loop finishes, it can just update this dictionary:

routes[i] = ...

And if you want a sorted flat array at the end:

let coordinates = steps.sorted { $0.0 < $1.0 }
    .flatMap { $0.1 }

Or, you might use a pre-populated array of optionals, where you can insert the particular route at the correct position in the array:

var routes: [[CLLocationCoordinate2D]?] = Array(repeating: nil, count: pointCount - 1)

And when you want to update one:

routes[i-1] = ...

And then, at the end, you can remove optionals with compactMap and flatten it with flatMap:

let coordinates = steps.compactMap { $0 }.flatMap { $0 }

Thus:

func fetchDirections(_ locations: [CLLocation], completion: @escaping ([CLLocationCoordinate2D]) -> Void) {
    let pointCount = locations.count

    guard pointCount > 1 else { return }

    var routes: [[CLLocationCoordinate2D]?] = Array(repeating: nil, count: pointCount - 1)
    let group = DispatchGroup()

    for i in 1 ..< pointCount {
        group.enter()
        directions(from: locations[i-1], to: locations[i]).calculate { response, error in
            defer { group.leave() }

            guard
                error == nil,
                let response = response,
                let route = response.routes.first
            else { return }

            routes[i-1] = self.coordinates(for: route.steps)
        }
    }

    group.notify(queue: .main) {
        let coordinates = routes.compactMap { $0 }.flatMap { $0 }
        completion(coordinates)
    }
}

func directions(from: CLLocation, to: CLLocation) -> MKDirections {
    let request = MKDirections.Request()
    request.source = MKMapItem(placemark: MKPlacemark(coordinate: from.coordinate))
    request.destination = MKMapItem(placemark: MKPlacemark(coordinate: to.coordinate))
    request.requestsAlternateRoutes = false
    request.transportType = .walking
    return MKDirections(request: request)
}

func coordinates(for steps: [MKRoute.Step]) -> [CLLocationCoordinate2D] {
    guard !steps.isEmpty else { return [] }

    var coordinates: [CLLocationCoordinate2D] = []

    for step in steps {
        let count = step.polyline.pointCount
        let pointer = step.polyline.points()
        for i in 0 ..< count {
            let coordinate = pointer[i].coordinate
            if coordinate.latitude != coordinates.last?.latitude, coordinate.longitude != coordinates.last?.longitude {
                coordinates.append(coordinate)
            }
        }
    }

    return coordinates
}

Where:

fetchDirections(locations) { coordinates in
    let polyline = MKPolyline(coordinates: coordinates, count: coordinates.count)
    self.mapView.addOverlay(polyline)
}

Yielding, for a stroll through Apple’s complex:

enter image description here


By the way, notice that I’m not just using the coordinate of the polyline of the MKRoute.Step. That’s the center of the polyline. You presumably want to iterate through the points().

That having been said, when I get directions, it’s generally just to show it on the map, so I generally just add the polyline as an overlay, directly, and don’t bother decomposing it into arrays of CLLocationCoordinate2D, but I assume you have some other reason for wanting to do that.

Rob
  • 415,655
  • 72
  • 787
  • 1,044
  • Many thanks for all the explanation. The reason why I'm doing all this is that GPS locations are very imprecise, so tracking routes using GSP leads to very poor and zig zagging tracking. The complete sequence I use is: 1st track the route. ( very crazy tracking) 2nd filter out low accuracy signals, non sequential signals, and too far or too close signals. ( much much better but still zig zagging, having points ending up anywhere.. like in the middle of a building..) 3rd (with this function) I kinda snap the filtered route to actual walking paths.. – Vincenzo Dec 22 '19 at 11:39
  • It's unbelievable how difficult is to get a tracking right. Took me months of trial and error to come up with this solution but is the base of my app, so this is the best Christmas present I'll get the year. Many thanks, it would have taken me a lot longer to come up with such an elegant solution for the snapping to road part. Sure I could have used the Google Map function .. but I rather stick with swift's maps. In the end, if Google can, we also can. – Vincenzo Dec 22 '19 at 11:43
  • apart for a couple of side deviations as the one on the left of the arch in your example, it works just perfectly. I'll try to come up with a function to clean that up and call it done. I' thinking of checking 3 points in a loop and if 1 and 3 have same coordinates or within a range, than eliminate 2. – Vincenzo Dec 22 '19 at 11:47
  • FWIW, that deviation on my path was deliberate. They walked from the fitness center, to the welcome center, and then to the theater, meaning that they actually did double back on their path. – Rob Dec 22 '19 at 14:35
  • I’d suggest that if you’re considering what data points to discard or smooth, you factor in the [`horizontalAccuracy`](https://developer.apple.com/documentation/corelocation/cllocation/1423599-horizontalaccuracy). You might want to liberally eliminate zig-zagging that is a result of poor GPS data, but not eliminate detours that the user actually made. – Rob Dec 22 '19 at 14:42
  • sure, I do filter out `horizontal accuracy < 0` as it is an invalid signal, `horizontal accuracy > 80` as I consider it too poor. The best `horizontal accuracy ` I get is 65 meters dough, so I still get a certain level of zig-zagging. In my case the deviations I get is the result of some location being registered in a side street, so it's not wanted, other than that it's just perfect. I noticed that if I play with the minimum and maximum distance in my filtering function I get better results, so I'll experiment a bit with values as I might not need to filter further from `fetchDirections` – Vincenzo Dec 23 '19 at 08:17
  • So far if I keep only signals that are within 25 to 45 meters from each other I get perfect results 99% of the times, so I might just consider it done.. – Vincenzo Dec 23 '19 at 08:19