Manual map management
Offline functionality for the Maps and Navigation SDKs for iOS is only available upon request. Contact us to get started.
An offline NDS map is divided into map regions that can be updated individually. Updating can be done automatically, as described in the Offline map setup guide. For some use cases, you may require more control over the map regions to be updated than what is automatically updated. 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 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 regions of an offline NDS map are organized in a hierarchical tree structure. This structure is represented by the RegionGraph
and the CompositeRegionGraph
, each offering a different perspective on the map structure.
- The
RegionGraph
provides a detailed and fine-grained representation of the map structure. Only the lowest-level map regions can be updated. - The
CompositeRegionGraph
hides the lowest-level map regions and allows updating at any level in the tree.
You can see the visual differences between the two representations in the following images:
Note that the map structure often varies between different maps, based upon the map provider. NDS Maps provided by TomTom are divided in small map regions to limit data and storage usage when updating automatically. However, these small lower-level map regions are not meant to be shown to the end user. Therefore, TomTom recommends using the CompositeRegionGraph
.
Fine-grained representation
The RegionGraph
is a tree of RegionGraphNode
instances. Each RegionGraphNode
represents a map region and has a localized name and ID. Information about the lower-level nodes is provided in RegionGraphNodeState
instances, such as:
- Whether the region is currently installed
- Whether there are available updates for the region
- The size of the update that can 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 a listener is first added, it will be called with the current map structure and map region states.
After instantiating the NDSStoreUpdater
, you can register the observer you created. To construct the NDSStoreUpdater
, refer to the Offline map quickstart.
let regionGraphObserver = MyRegionGraphObserver()ndsStoreUpdater.addRegionGraphObserver(regionGraphObserver)
Performing map operations
The SDK provides an API that allows you to perform map operations, including installing, updating, and uninstalling map regions.
Note that only the lowest-level 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. 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}
Each RegionGraphNode
is identified by a RegionGraphNodeID
. 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.
Canceling map operations
If you want to cancel one or more map operations, you can do that via 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}
This cancels scheduled map operations, as well as automatic map updates.
Composite representation
The CompositeRegionGraph
is a tree of (CompositeRegionUrl}[CompositeRegion
] nodes. Each node represents a map region and has a localized name and ID. Information about each node is provided in CompositeRegionState
instances, such as:
- Whether the region is currently installed
- Whether there are available updates for the region
- The size of the update that can be downloaded.
Observing map structure and map region states
By implementing the CompositeRegionObserver
, you can observe changes in the CompositeRegionGraph
structure and the state of individual CompositeRegion
nodes:
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}
When a listener is first added, it is called with the current map structure and map region states.
When opting for this composite representation, you need to instantiate the CompositeRegionsUpdater
object first. The listener can be added to that object:
1let compositeRegionsUpdater = CompositeRegionsUpdater(ndsStoreUpdater: ndsStoreUpdater)2let compositeRegionObserver = MyCompositeRegionObserver()3compositeRegionsUpdater.addCompositeRegionObserver(compositeRegionObserver)
Performing map operations
The SDK provides an API that allows you to perform map operations, including installing, updating, and uninstalling map regions. When using CompositeRegionsUpdater
, you can perform operations on any node in the map structure tree.
Each CompositeRegion
is identified with a CompositeRegionID
. For each CompositeRegion
, you can perform map operations via the CompositeRegionsUpdater.schedule(operations:)
method of the CompositeRegionsUpdater
object. Here is an example:
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}
NOTE: If automatic map updates are also in progress, the scheduled map operations take priority over those initiated by automatic map updates.
Canceling map operations
If you want to cancel one or more map operations, you can do that via the CompositeRegionsUpdater.cancelAllMapOperations(for:)
method of the CompositeRegionsUpdater
. This cancels scheduled map operations, as well as automatic map updates.
UI suggestions for showing the map structure and map region states
Now that you have the data of map structure and the map region states, here are some suggestions for a user-interface for it using a dynamic list.
First, define the data model that contains the necessary information about each map region; including the name, CompositeRegionID
, CompositeRegionState
and child map regions. Each CompositeRegionID
is unique across the entire CompositeRegionGraph
.
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 have defined. In this example, we display the region’s name and the installation state.
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: CompositeRegionState?) -> 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] =4 init(mapRegions: [MapRegion] = []) {5 self.mapRegions = mapRegions6 }7}
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.
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.