Manual map management

VERSION 0.45.0
PUBLIC PREVIEW

Offline functionality for the Maps and Navigation SDKs for iOS is only available upon request. Contact us to get started.

For some use cases, you may require more control over the map regions to be updated than what automatic map updates provide. In such cases, this SDK offers a manual map management functionality.

The term manual comes from the common use case of a navigation application, where users are presented with a screen that displays a list of map regions, allowing them to manually select the regions they wish to update. However, manual map management can also be implemented based on your logic and may not necessarily require user interaction.

This guide provides an overview of how the map structure is represented and explains the steps to perform map operations on the map regions.

Map structure

The structure of a map is represented by the RegionGraph and the CompositeRegionGraph, each offering a different perspective on the map structure.

You can choose to use either representation based on your needs. The RegionGraph provides a detailed and fine-grained representation of the map structure. On the other hand, the CompositeRegionGraph provides a higher-level and more streamlined view of the map structure.

Both RegionGraph and CompositeRegionGraph follow a tree structure. In the RegionGraph, each node is represented by a RegionGraphNode, while in the CompositeRegionGraph, each node is represented by a CompositeRegion.

You can see the visual differences between the two representations in the following images:

It’s important to note that the map structure may vary depending on factors such as the map provider, map versions, and other considerations. The specific regions and their hierarchy can differ based on these factors.

Map region state

The state of the RegionGraphNode is represented by the RegionGraphNodeState. Similarly, the state of CompositeRegion is represented by the CompositeRegionState. They provide valuable information that can assist you in determining which regions to install or update, such as whether the region is currently installed, whether there are available updates for the region, and the size of the update that must be downloaded.

Observing map structure and map region states

By implementing the RegionGraphObserver, you can receive notifications whenever there are changes in the RegionGraph structure or the state of individual RegionGraphNode:

1class MyRegionGraphObserver: RegionGraphObserver {
2 func onRegionGraphChanged(event: TomTomSDKNDSStoreUpdater.RegionGraphChangeEvent) {
3 switch event {
4 case let .error(errorData):
5 // Error handling - Your code goes here.
6 // For example, print the error.
7 print(errorData.error.description)
8 case let .structureChanged(structureData):
9 // Your code goes here.
10 // For example, save the graph and the corresponding states.
11 graph = structureData.graph
12 nodeStates = structureData.nodeStates
13 case let .nodesStateChanged(nodesStateData):
14 // Your code goes here.
15 // For example, update the node states.
16 nodeStates.merge(nodesStateData.nodeStates) { _, new in new }
17 }
18 }
19
20 private var graph: RegionGraph?
21 private var nodeStates: [RegionGraphNodeID: RegionGraphNodeState] = [:]
22}

When the NDSStoreUpdater is instantiated, you can register the observer you’ve created. For constructing the NDSStoreUpdater, refer to the Offline map quickstart.

let regionGraphObserver = MyRegionGraphObserver()
ndsStoreUpdater.addRegionGraphObserver(regionGraphObserver)

Similarly, by implementing the CompositeRegionObserver, you can observe the changes in the CompositeRegionGraph structure or the state of individual CompositeRegion:

1class MyCompositeRegionObserver: CompositeRegionObserver {
2 func onCompositeRegionGraphChanged(event: TomTomSDKNDSStoreUpdater.CompositeRegionGraphChangeEvent) {
3 switch event {
4 case let .error(error):
5 // Error handling - Your code goes here.
6 // For example, print the error.
7 print(error.description)
8 case let .graphChanged(graphData):
9 // Your code goes here.
10 // For example, save the graph and the corresponding states.
11 graph = graphData.graph
12 regionStates = graphData.regionStatesData.stateMap
13 case let .statesChanged(stateData):
14 // Your code goes here.
15 // For example, update the node states.
16 regionStates.merge(stateData.stateMap) { _, new in new }
17 }
18 }
19
20 private var graph: CompositeRegionGraph?
21 private var regionStates: [CompositeRegionID: CompositeRegionState] = [:]
22}

To register the observer, you need to construct the CompositeRegionsUpdater first:

1let compositeRegionsUpdater = CompositeRegionsUpdater(ndsStoreUpdater: ndsStoreUpdater)
2let compositeRegionObserver = MyCompositeRegionObserver()
3compositeRegionsUpdater.addCompositeRegionObserver(compositeRegionObserver)

Representing map structure and map region states

Now that you have the data of map structure and the map region states, you can proceed to represent them. Here is an example of using a dynamic list to display the map structure:

First, define the data model that contains the necessary information about each map region, including the name, RegionGraphNodeID, RegionGraphNodeState, and child map regions, etc.

Note that to conform to the Identifiable protocol, the Identifiable.id must be unique for each item in the list. However, the RegionGraphNodeID may not be unique across the entire graph, as it can appear multiple times with different parent nodes.

To ensure uniqueness of each map region, there are different approaches you can take. One option is to combine RegionGraphNodeID of each map region with the RegionGraphNodeID of its parent region. Alternatively, as shown in the example, use UUID.

1class MapRegion: ObservableObject, Identifiable {
2 let id = UUID()
3 let name: String
4 let nodeID: RegionGraphNodeID
5 @Published
6 var state: RegionGraphNodeState?
7 let children: [MapRegion]?
8
9 init(node: RegionGraphNode) {
10 self.name = node.name
11 self.nodeID = node.id
12 self.children = node.children?.map {
13 MapRegion(node: $0)
14 }
15 }
16}

In the case of using the CompositeRegion, because each CompositeRegionID is unique across the entire CompositeRegionGraph, you can define the data model as in the following example:

1class MapRegion: ObservableObject, Identifiable {
2 let id: CompositeRegionID
3 let name: String
4 @Published
5 var state: CompositeRegionState?
6 let children: [MapRegion]?
7
8 init(node: CompositeRegion) {
9 self.name = node.name
10 self.id = node.id
11 self.children = node.children?.map {
12 MapRegion(node: $0)
13 }
14 }
15}

Next, define a custom view for the data model you’ve defined. In this example, we display the region’s name and the installation state for the RegionGraphNode. You can adapt the code for the CompositeRegion by replacing the RegionGraphNodeState with CompositeRegionState.

1struct MapRegionView: View {
2 @StateObject
3 var mapRegion: MapRegion
4 var body: some View {
5 Text(mapRegion.name)
6 Spacer()
7 Text(getInstallStateText(state: mapRegion.state))
8 }
9
10 func getInstallStateText(state: RegionGraphNodeState?) -> String {
11 switch state?.installState {
12 case .completelyInstalled:
13 return "Completely installed"
14 case .inconsistent:
15 return "Inconsistent"
16 case .notInstalled:
17 return "Not Installed"
18 case .partiallyInstalled:
19 return "Partially Installed"
20 default:
21 return ""
22 }
23 }
24}

Then we define the data model for the dynamic list:

1class MapRegionsViewModel: ObservableObject {
2 @Published
3 var mapRegions: [MapRegion] = }

And the list view:

1struct MapRegionsView: View {
2 @StateObject
3 var viewModel: MapRegionsViewModel
4 var body: some View {
5 if viewModel.mapRegions.isEmpty {
6 Text("Loading ...")
7 } else {
8 List(viewModel.mapRegions, children: \.children) {
9 MapRegionView(mapRegion: $0)
10 }
11 }
12 }
13}

Note, you must update the model of the dynamic list whenever the map structure changes. To observe the changes in the map structure, refer to the section Observing map structure and map region states.

Performing map operations

The SDK provides an API that allows you to perform map operations such as installing/updating, and uninstalling map regions on RegionGraphNode.

Note that not all nodes present in the RegionGraph support these map operations. For each RegionGraphNode, you can check the RegionGraphNode.isUpdatableRegion property to identify if the node is eligible for map operations. RegionGraphNode.isUpdatableRegion is a read-only property that returns true if the RegionGraphNode supports map operations and false otherwise.

Here is an example of identifying all the updatable map regions under a specific RegionGraphNode:

1func findAllUpdatableNodes(node: RegionGraphNode) -> [RegionGraphNodeID] {
2 var updatableNodes: [RegionGraphNodeID] =
3 if node.isUpdatableRegion {
4 updatableNodes.append(node.id)
5 } else {
6 node.children?.forEach {
7 updatableNodes += findAllUpdatableNodes(node: $0)
8 }
9 }
10 return updatableNodes
11}

For each updatable RegionGraphNode, you can perform map operations using NDSStoreUpdater.schedule(operations:). Here is an example:

1// Schedule install and update for all the updatable regions that belong to the first root.
2var mapOperations: [MapOperation] = if let firstRoot = graph.roots.first {
3 let regionsToInstall = findAllUpdatableNodes(node: firstRoot)
4 for node in regionsToInstall {
5 mapOperations.append(MapOperation(type: .installAndUpdate, nodeID: node))
6 }
7}
8
9// Schedule uninstall for all the updatable regions that belong to the last root.
10if let lastRoot = graph.roots.last {
11 let regionsToUninstall = findAllUpdatableNodes(node: lastRoot)
12 for node in regionsToUninstall {
13 mapOperations.append(MapOperation(type: .uninstall, nodeID: node))
14 }
15}
16ndsStoreUpdater.schedule(operations: mapOperations)

NOTE: If automatic map updates are also in progress, the scheduled map operations take priority over those initiated by automatic map updates.

Similarly, you can use the CompositeRegionsUpdater.schedule(operations:) method to perform map operations on the CompositeRegion. Note that all the CompositeRegion support map operations. You can schedule map operations on any CompositeRegion like the following code snippets:

1// Schedule install & update for the first root and all its child nodes
2if let firstRoot = graph.roots.first {
3 let installOperation = CompositeRegionOperation(id: firstRoot.id, operationType: .installAndUpdate)
4 compositeRegionsUpdater.schedule(operations: [installOperation])
5}

Canceling map operations

If you want to cancel the scheduled map operations, you can do it with the NDSStoreUpdater.cancelAllMapOperations(for:) method. Here is an example:

1// Cancel all the map operations for all the map regions that belong to the first root.
2if let firstRootNode = graph.roots.first {
3 let nodesToCancel = findAllUpdatableNodes(node: firstRootNode)
4 ndsStoreUpdater.cancelAllMapOperations(for: nodesToCancel)
5}

Similarly, you can use the CompositeRegionsUpdater.cancelAllMapOperations(for:) method to cancel the scheduled map operations on the CompositeRegion.

Next steps

Since you have learned about manual map management on offline maps, here are recommendations for the next step:

By diving deeper into these areas, you can unlock the full potential of offline maps in your application.