mapbox / mapbox-navigation-ios

Turn-by-turn navigation logic and UI in Swift on iOS
https://docs.mapbox.com/ios/navigation/
Other
861 stars 310 forks source link

[Bug] Offline guided navigation not working for some routes #4419

Open kristianpennacchia opened 1 year ago

kristianpennacchia commented 1 year ago

Hello,

I have been using MapBox guided navigation (turn-by-turn) successfully in my app for a while, and am now looking to implement offline guided navigation.

I have configured predictive caching for both map and navigation, and implemented the manual TileStore region caching following your tutorial and example code.

Unfortunately, I have not been able to get offline guided navigation to work consistently.

Packages: MapboxNavigation 2.12.0-rc1 MapboxMaps 10.12.1 and all of their dependencies.

Steps I take to encounter issue:

  1. Perform the TileStore caching for a route.
  2. Run the guided navigation for a few minutes to let predictive caching do its thing.
  3. Close the app.
  4. Disconnect from the internet.
  5. Reopen the app and attempt to start guided navigation again for a route which should now be cached.
  6. Guided navigation will start for some routes, but for others I will receive an error when trying to calculate directions for the route.

I'm not sure why some routes will work offline but not others. It is easier to reproduce when using very long routes e.g. 50km, but can also happen on short routes e.g. 2km.

I know the caching is successful as I have logged the TileStore response from tileStore.loadTileRegion(forId:loadOptions:) and have looked into the apps documents and caches folders and can see the map and navigation data being created and updated.

I'm encountering this issue when attempting to calculate the directions for a route using Directions.calculateWithCache(options:).

The error that I get back when the directions calculation fails for this route is always:

[Mapbox] [Error, nav-native]: getRoute("https://api.mapbox.com/directions/v5/mapbox/driving/144.966347,-37.815031%3B144.231977,-36.594836%3B143.756608,-35.545444?alternatives=true&continue_straight=true&roundabout_exits=true&exclude=unpaved&geometries=polyline6&overview=full&steps=true&language=en-US&voice_instructions=true&voice_units=imperial&banner_instructions=true&annotations=duration,maxspeed,congestion_numeric&waypoints=0;2&access_token=****sDBA") id 0 failed: RouterException No suitable edges near location

It sounds like it is saying that there is not enough cached data to calculate directions for this route, but all of the coordinates, including the users location are within the cached regions via TileStore caching.

For predictive caching, I have set it to be very generous on the radius it caches to see if that solves the problem, but it does not.

For TileStore region caching, I cache a bounding box which encompasses all of the relevant coordinates for the route including the users current location.

I'm not sure if I'm missing something, or if it's a bug in the SDK. I've gone through the tutorial and example code a bunch of times but it all seems fine.

I don't want to flood this ticket with tons of code, so I'm going to try to post only the relevant snippets of code that I'm using for the TileStore region caching.

let tileStore = TileStoreConfiguration.Location.default.tileStore
tileStore.setOptionForKey(
    TileStoreOptions.mapboxAccessToken,
    domain: .maps,
    value: BuildConfig.mapBoxAccessToken
)
tileStore.setOptionForKey(
    TileStoreOptions.mapboxAccessToken,
    domain: .navigation,
    value: BuildConfig.mapBoxAccessToken
)

// Set disk quota to 200MB.
tileStore.setOptionForKey(
    TileStoreOptions.diskQuota,
    value: 209715200
)

let offlineManager = OfflineManager(
    resourceOptions: ResourceOptions(
        accessToken: BuildConfig.mapBoxAccessToken,
        tileStore: tileStore,
        tileStoreUsageMode: .readOnly
    )
)

[...] Style pack is then cached using offline manager [...]

let styleURI = StyleURI(rawValue: BuildConfig.mapBoxStyleUrl)!
let tilesetDescriptorOptions = TilesetDescriptorOptions(
    styleURI: styleURI,
    zoomRange: 5...12,
    stylePackOptions: stylePackLoadOptions
)

// Create a bounding box encompassing all coordinates from all stages in this drive.
// All tiles within this bounding box will be cached.
let bboxNorth = allCoordinates.map(\.latitude).max()!
let bboxSouth = allCoordinates.map(\.latitude).min()!
let bboxEast = allCoordinates.map(\.longitude).max()!
let bboxWest = allCoordinates.map(\.longitude).min()!

// Coordinates in this outer ring must be counter-clockwise.
// https://www.rfc-editor.org/rfc/rfc7946#section-3.1.6
let outerRing = Ring(coordinates: [
    LocationCoordinate2D(latitude: bboxNorth, longitude: bboxEast),
    LocationCoordinate2D(latitude: bboxNorth, longitude: bboxWest),
    LocationCoordinate2D(latitude: bboxSouth, longitude: bboxWest),
    LocationCoordinate2D(latitude: bboxSouth, longitude: bboxEast),
    LocationCoordinate2D(latitude: bboxNorth, longitude: bboxEast),
])
let polygon = Polygon(outerRing: outerRing)

let tilesetDescriptor = offlineManager.createTilesetDescriptor(for: tilesetDescriptorOptions)
let tileRegionLoadOptions = TileRegionLoadOptions(geometry: polygon.geometry, descriptors: [tilesetDescriptor], acceptExpired: false)!
let tileRegionId = "\(event.id.uuidString)_\(drive.id.uuidString)"

tileStore.tileRegion(forId: tileRegionId) { result in
    switch result {
    case .success(_):
        log.info("Tile region is already cached.")
    case .failure(_):
        log.info("Caching tiles for \(drive.routes.count) stages in drive '\(drive.name)'...")
        tileStore.loadTileRegion(forId: tileRegionId, loadOptions: tileRegionLoadOptions) { result in
            [...]
        }
    }
}
j-cimb-barker commented 1 year ago

Yes, I am also experiencing this. If I try and obtain offline directions more than once I get the same "RouterException No suitable edges near location" until I restart the App.

Udumft commented 1 year ago

Hi, @kristianpennacchia !

tileStore.setOptionForKey( TileStoreOptions.mapboxAccessToken, domain: .maps, value: BuildConfig.mapBoxAccessToken ) tileStore.setOptionForKey( TileStoreOptions.mapboxAccessToken, domain: .navigation, value: BuildConfig.mapBoxAccessToken )

// Set disk quota to 200MB. tileStore.setOptionForKey( TileStoreOptions.diskQuota, value: 209715200 )

You don't need to do this ^ Tilestore setup code as SDK already provides you with configured instance. 200MB is basically a tile size of 1 city, so in real life, given ambient cache (which spans ~50km around user location), this is not too big storage space which can lead to some tiles dropped and result in some routes built correctly, while others (even shorter ones) are not. So it's better to stick with default configuration. Also, please be sure to use same tilestore instance across all places. Rule of thumb here is to use NavigationSettings.shared.tileStoreConfiguration and stick with 'default' config if there is no special need to not to. And last advice would be to use MapboxRoutingProvider (with source = .hybrid (default)) for requesting routes.