Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

TileStore config for Navigator and MapView #2956

Merged
merged 6 commits into from May 5, 2021

Conversation

Udumft
Copy link
Contributor

@Udumft Udumft commented Apr 27, 2021

Description

Resolves #2926
Adds setting to control Navigator and MapView tile store location.

Implementation

Navigator TileStore can be configured only before it is initialised. There are 2 entry points which instantiate a Navigator instance and it is never deallocated (for now): FreeDrive (aka PassiveLocationDataSource and Active Guidance (aka RouteController). Navigator always has to have a tile storage somewhere. MapView is handled by NavigationMapViewwhich can be used separately or as a part ofNavigationViewController`. MapView can have no on-device tile storage at all.

@Udumft Udumft requested a review from a team April 27, 2021 15:51
@Udumft Udumft self-assigned this Apr 27, 2021
/**
To add docs for cases
*/
public enum TileStoreLocation {
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I am concerned about this thing. From one perspective, since Map and Navigator have slightly different use-cases (Map may have no TileStore) using URL? as a setting does not cover all possible cases. In this case nil for Nav == default, while nil for map == no tile store.
Introducing this enum construct allows fine control and convenience usage like:

options.tileStoreLocation = .custom(url) // to pass user-provided location, or
options.tileStoreLocation = .isolated(.default, noStorage) // to disable Map Tile storage while keeping Nav at default

On the other hand - this setting is not reusable while manipulating bare NavigationView or NavigationService. It is also very sensitive to initialisation order. If User wants to create his custom NavigationService to pass it in NavigationOptions - he has to also take care of configuring a tile storage parameter.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

And yes, naming is an open question

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

An enumeration with associated values seems reasonable to me.

this setting is not reusable while manipulating bare NavigationView or NavigationService

Not sure I follow – we could modify those classes to accept a TileStoreLocation too, right?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not sure I follow – we could modify those classes to accept a TileStoreLocation too, right?

In this case we should frame it outside NavigationOptions to avoid uglyNavigationService.init definition, and it makes TileStoreLocation a standalone type which I'd like to avoid. Also, in terms of bare NavigationMapView plain TileStore? is a more robust way for picking storage location.

@@ -285,6 +305,8 @@ open class NavigationViewController: UIViewController, NavigationStatusPresenter

var viewObservers: [NavigationComponentDelegate] = []

var mapTileStore: TileStore?
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Had to store it here to be able to reach it during loadView and configure NavigationView. Tile storage is configured at initialisation time for MapView which forces piping this as init parameter through NavigationView and NavigationMapView. Pretty much the same happens for Navigator.
Apart of ravaging the init API, it also makes tile store setup very sensitive to init order. If any component is created before NavigationViewController.init - it will ignore NavigationOptions setup.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If any component is created before NavigationViewController.init - it will ignore NavigationOptions setup.

This suggests that maybe those components should also accept a NavigationOptions in their initializers, or that the tile store should be independent of NavigationOptions.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

that the tile store should be independent of NavigationOptions

Not sure to follow this part. Did you mean to extract TileStore configuration from NavigationOptions? But that ruins concept of NavigationOptions as single customization source.
Having components accept NavigationOptions seems and overkill since it just needs a TileStore bit. But it brings us back to having TileStoreLocation as a standalone type suitable for configuring components. I guess I'll try this case

/**
To add docs for cases
*/
public enum TileStoreLocation {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

An enumeration with associated values seems reasonable to me.

this setting is not reusable while manipulating bare NavigationView or NavigationService

Not sure I follow – we could modify those classes to accept a TileStoreLocation too, right?


case `default`
case custom(URL)
case isolated(NavigatorStorage, MapStorage)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This case would be clearer with labels for each of the associated values.

*/
public convenience init(styles: [Style]? = nil, navigationService: NavigationService? = nil, voiceController: RouteVoiceController? = nil, topBanner: ContainerViewController? = nil, bottomBanner: ContainerViewController? = nil, predictiveCacheOptions: PredictiveCacheOptions? = nil) {
public convenience init(styles: [Style]? = nil, navigationService: NavigationService? = nil, voiceController: RouteVoiceController? = nil, topBanner: ContainerViewController? = nil, bottomBanner: ContainerViewController? = nil, predictiveCacheOptions: PredictiveCacheOptions? = nil, tileStoreLocation: TileStoreLocation = .default) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There are a lot of arguments with default values. If the default is sensible in most cases, we could omit this argument from the initializer and expect developers to set the tileStoreLocation property on a separate line.

This goes for the rest of the arguments too. I’m not sure why we really need a convenience initializer, since there’s no inherent order to the arguments other than the order in which we implemented each feature. v2.0 would be a good opportunity to remove this initializer, though maybe in a separate PR.

@@ -285,6 +305,8 @@ open class NavigationViewController: UIViewController, NavigationStatusPresenter

var viewObservers: [NavigationComponentDelegate] = []

var mapTileStore: TileStore?
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If any component is created before NavigationViewController.init - it will ignore NavigationOptions setup.

This suggests that maybe those components should also accept a NavigationOptions in their initializers, or that the tile store should be independent of NavigationOptions.

@Udumft
Copy link
Contributor Author

Udumft commented Apr 29, 2021

I've updated the code to utilise TileStoreLocation in all components where TileStore setting is required. Hopefully it will be clearer for users what kind of setting is required and help avoid initialisation order issues.

if such solution seems reasonable, I'll go on and finalize it.

@Udumft Udumft requested review from 1ec5 and a team April 29, 2021 07:57
Base automatically changed from vk/906-tilestore-expose to release-v2.0 April 29, 2021 12:58
case `default`
case custom(URL)

public var tilStoreURL: URL? {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

small typo here

public var tilStoreURL: URL? {
switch self {
case .default:
return nil
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Those enums seem too sophisticated IMHO, especially the fact that Strict.tileStoreURL can be nil.
I thought that Strict enum is used to explicitly show that the url is always present.
I wonder if there is a way to simplify code here.

@Udumft Udumft requested a review from bamx23 April 29, 2021 16:42
/**
To add docs for cases
*/
public enum TileStoreLocation {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I have an idea for this class:

public struct TileStoreConfiguration {
    enum Location: RawRepresentable {
        case `default`
        case custom(URL)

        init?(rawValue: URL?) {
            self = rawValue.map { .custom($0) } ?? .default
        }

        var rawValue: URL? {
            switch self {
            case .default:
                return nil
            case .custom(let url):
                return url
            }
        }

        var tileStore: TileStore {
            switch self {
            case .default:
                return TileStore.getInstance()
            case .custom(let url):
                return TileStore.getInstanceForPath(url.path)
            }
        }
    }

    var navigatorLocation: Location
    var mapLocation: Location?

    static var `default`: Self {
        .init(navigatorLocation: .default, mapLocation: .default)
    }
    static func custom(_ url: URL) -> Self {
        .init(navigatorLocation: .custom(url), mapLocation: .custom(url))
    }
    static func isolated(navigationLocation: Location, mapLocation: Location?) -> Self {
        .init(navigatorLocation: navigationLocation, mapLocation: mapLocation)
    }
}

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I like this! I think I'll try this in next iteration

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure if Location enum needs RawRepresentable, but it looks cooler :) But also this makes some ambiguity for let x = Location(rawValue: nil) vs let x: Location? = nil.

/**
Location of Map tiles data
*/
public let mapLocation: Location?
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is it necessary to have it optional? The only place where it is used has a fallback to .default in case it's nil. Maybe it's better to leave it non-optional and if the user doesn't want to provide this they just use .default.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, it should have an option to be nil. Map may have no local tile store. So to distinguish default and no tilestore we need it optional.
But you are also right about the only place it is used and is defaulted - that's a bug :)

@@ -47,6 +47,12 @@ open class NavigationOptions: NavigationCustomizable {
*/
open var predictiveCacheOptions: PredictiveCacheOptions?
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

PredictiveCacheManager can also initialize a Navigator instance.

And also I see some work with tile store in NavigationMapView.enablePredictiveCaching. Oh, nevermind, it's taken into consideration in the NavigationMapView constructor.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good catch! Even though NavigationMapView.enablePredictiveCaching is covered by constructor, PredictiveCacheManager is publicly available and thus can potentially be called before any other setup is done.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It worth thinking about how to avoid such potential problems in the future. Maybe it can be solved by extracting Navigator pre-initialization config into a separate struct to make a single entry point that at least easier to validate in Xcode by inspecting "show all callers" or something like that.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I thought about that, but I see there are few issues: either we force this struct to be configured before any interaction w/ SDK (technically we introduce an SDK setup step) and trust user to do so at proper moment, or we encapsulate this setup anywhere where it may be triggered which makes the process transparent for users, but requires us to maintain all such places. What we are doing currently looks more like a latter case.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I understand why you've done it this way. What I'm talking about is to make future internal changes safer, to reduce the chance of forgetting to put property initiation into some of the places where a Navigator instance can be created.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we should better frame it outside current PR, because this seems to me like a complex solution, covering more than tile store configuration.

@@ -47,6 +47,12 @@ open class NavigationOptions: NavigationCustomizable {
*/
open var predictiveCacheOptions: PredictiveCacheOptions?
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

And, by the way, we have ...Options semantic here. Maybe it's better to keep this in this PR and rename TileStoreConfiguration into TileStoreOptions?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Unfortunately it will cause naming conflict with Common's MBXTileStoreOptions. I would like to avoid namespace usage.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I agree that “options” would be a more consistent naming choice. Internal type conflicts ideally shouldn’t influence what we expose publicly to developers, but this would be an opportunity to prefer the more grammatical “TileStorage” terminology over “TileStore” (which incidentally sounds like a kind of shop).

Udumft added 4 commits May 3, 2021 15:06
…pproach. Piped tile store settings through to Navigator and NavigationMapView.
…ype. Updated Tile Store configs to accept TileStoreLocation item.
…nfiguration. Added code docs. Added config for PassiveLocationDataSource.
…tiveCaching setup. Removed TileStoreConfiguration RawRepresentable adoption, fixed NavigationViewController.mapTileStore setup.
@Udumft Udumft force-pushed the vk/2926-tilestore-config branch from da05577 to 35d7027 Compare May 3, 2021 12:16
@Udumft
Copy link
Contributor Author

Udumft commented May 3, 2021

I think I've covered all entry points to Navigator initialization now. Indeed, that might become complicated to add more settings or maintain them. We might consider introducing some kind of a setup mechanism for such things to ease the support and reduce potential risk of human error.

@Udumft Udumft requested a review from bamx23 May 3, 2021 12:20
@Udumft Udumft marked this pull request as ready for review May 3, 2021 15:02
@Udumft Udumft requested a review from a team May 3, 2021 15:03
@Udumft Udumft removed the wip label May 4, 2021
Copy link
Contributor

@1ec5 1ec5 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think there remain some opportunities for making the API more ergonomic for developers, but they can be handled as tail work. The overall functionality is in good shape.

CHANGELOG.md Outdated
@@ -68,6 +68,7 @@
* Exposed `NavigationMapView.mapTileStore`, `PassiveLocationDataSource.navigatorTileStore` and `RouteController.navigatorTileStore` for accessing corresponding `TileStore` instancies ([#2955](https://github.com/mapbox/mapbox-navigation-ios/pull/2955))
* Removed `ElectronicHorizon` struct, now an electronic horizon notification directly contain pointer to a starting edge. `ElectronicHorizon.Edge` was renamed to `RoadGraph.Edge`. `ElectronicHorizon.NotificationUserInfoKey` was renamed to `RoadGraph.NotificationUserInfoKey`. ([#2949](https://github.com/mapbox/mapbox-navigation-ios/pull/2949))
* Fixed an issue that the current road name label flashes when camera state changes or travels onto an unnamed road. ([#2958](https://github.com/mapbox/mapbox-navigation-ios/pull/2958))
* Introduced `TileStoreConfiguration` struct for setting up tile storage locations for Navigation and Map tiles. You can customise it via `NavigationOptions.tileStoreConfiguration`, `PassiveLocationDatasource.init(directions: Directions, systemLocationManager:, tileStoreLocation:)`, `NavigationMapView.init(frame:, navigationCameraType:, tileStoreLocation:)`, `PredictiveCacheManager.init(predictiveCacheOptions:, tileStoreMapOptions:)` or others, accepting `TileStoreConfiguration`. ([#2956](https://github.com/mapbox/mapbox-navigation-ios/pull/2956))
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
* Introduced `TileStoreConfiguration` struct for setting up tile storage locations for Navigation and Map tiles. You can customise it via `NavigationOptions.tileStoreConfiguration`, `PassiveLocationDatasource.init(directions: Directions, systemLocationManager:, tileStoreLocation:)`, `NavigationMapView.init(frame:, navigationCameraType:, tileStoreLocation:)`, `PredictiveCacheManager.init(predictiveCacheOptions:, tileStoreMapOptions:)` or others, accepting `TileStoreConfiguration`. ([#2956](https://github.com/mapbox/mapbox-navigation-ios/pull/2956))
* Added the `NavigationOptions.tileStoreConfiguration` property and arguments to `PassiveLocationDataSource(directions:systemLocationManager:tileStoreLocation:)`, `NavigationMapView(frame:navigationCameraType:tileStoreLocation:)`, and `PredictiveCacheManager(predictiveCacheOptions:tileStoreMapOptions:)` for customizing the locations where navigation and map tiles are stored. ([#2956](https://github.com/mapbox/mapbox-navigation-ios/pull/2956))

import MapboxCommon

/**
Describes `TileStore` setup.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is TileStore a class that the developer would work with directly? If not, use a more generic term for it:

Options for configuring how map and navigation tiles are stored on the device.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It is. This class is exposed to user and is a main point for Offline Regions feature.

Comment on lines +16 to +25
/**
Encapsulated default location.

`rawValue` for this case will return `nil`
*/
case `default`
/**
User-provided path to tile storage folder
*/
case custom(URL)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Similar to mapbox/mapbox-maps-ios#252, I think it would be more ergonomic for Location to be a raw-representable struct with a single default static constant, or we could even reuse the URL type, treating nil as the default value and extending the type for properties such as tileStoreURL.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Using URL type is problematic because we cannot exactly say that nil == default value. Map and Navigator treat nil differently: Navigator will switch to a default location while Map may have no tile storage at all.
IMHO in this case an enum with associated value is more descriptive than a struct with static constant.

@@ -47,6 +47,12 @@ open class NavigationOptions: NavigationCustomizable {
*/
open var predictiveCacheOptions: PredictiveCacheOptions?
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I agree that “options” would be a more consistent naming choice. Internal type conflicts ideally shouldn’t influence what we expose publicly to developers, but this would be an opportunity to prefer the more grammatical “TileStorage” terminology over “TileStore” (which incidentally sounds like a kind of shop).

@Udumft Udumft merged commit b309f2e into release-v2.0 May 5, 2021
@Udumft Udumft deleted the vk/2926-tilestore-config branch May 5, 2021 08:03
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

4 participants