Manual map management
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.graph12 nodeStates = structureData.nodeStates13 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 }1920 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.graph12 regionStates = graphData.regionStatesData.stateMap13 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 }1920 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: String4 let nodeID: RegionGraphNodeID5 @Published6 var state: RegionGraphNodeState?7 let children: [MapRegion]?89 init(node: RegionGraphNode) {10 self.name = node.name11 self.nodeID = node.id12 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: CompositeRegionID3 let name: String4 @Published5 var state: CompositeRegionState?6 let children: [MapRegion]?78 init(node: CompositeRegion) {9 self.name = node.name10 self.id = node.id11 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 @StateObject3 var mapRegion: MapRegion4 var body: some View {5 Text(mapRegion.name)6 Spacer()7 Text(getInstallStateText(state: mapRegion.state))8 }910 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 @Published3 var mapRegions: [MapRegion] = }
And the list view:
1struct MapRegionsView: View {2 @StateObject3 var viewModel: MapRegionsViewModel4 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 updatableNodes11}
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}89// 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 nodes2if 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:
- Adding Traffic Support: Learn more about how to include traffic information.
- Adding Search Support: Learn more about integrating search features into your map.
By diving deeper into these areas, you can unlock the full potential of offline maps in your application.