Use Maps Offline

With preloaded offline maps you can access whole countries or even continents completely offline without using any internet connection or consuming OTA bandwith.

Offline maps (also known as persistent maps) offer the same features as the map data that is available online: You can search for places, calculate routes, start guidance (only available for the Navigate Edition), and - of course - you can interact with the map in the same way as with online map data.

Note

This is a beta release of this feature, so there could be a few bugs and unexpected behaviors. APIs may change for new releases without a deprecation process. Currently, only car routes are supported.

Why use offline maps? There can be situations when an internet connection may lag, drop - or is completely off for a longer time. Especially, while on-the-go, a stable connection may not always be available. A mobile device may also be offline due to the user's decision to save bandwidth. This means: There can be multiple reasons to use offline maps - and last but not least, offline maps will speed up the user's experience with unbeatable response times. However, in most situations - when limited bandwidth and data consumption is not an issue - accessing online map data offers more accurate and up-to-date map data. Also, some features may not be available on offline maps, for example, search results may contain not all data that is available online.

You have two ways to access map data offline:

  • Local Cache: By default, all map data is cached onto the device while using the map. This storage is persisted between sessions, but the storage size is limited and old cache data may be replaced with newer map data. Note: You can change the default cache path and the size with the SDKOptions you can pass into the SDKNativeEngine when you initialize the HERE SDK programmatically. Cached map data is stored in the map cache storage.

  • Offline Maps: With offline maps you can download entire regions or even continents to preload their map data for offline use - including places, routing and other data. A dedicated MapDownloader enables you to get and to maintain this data. Offline maps data is persisted between sessions and the data will not be deleted unless the user decides so. Offline maps are stored in the persistent map storage.

Note

Offline maps work for all map schemes that are vector based. Satellite based map schemes are not part of the downloaded map data.

Good to know:

While interacting with a map view, the HERE SDK will first check if downloaded offline map data is available and show it. If no offline maps are available for the current target, the HERE SDK will download online map data and cache it. If no online connection is available, it will try to find data in the map data cache. If even that fails, the map will be shown with less details or even no details.

Note

When the HERE SDK uses backend services such as for search and routing, you need to use dedicated offline engines to access cached or predownloaded offline map data. Use the OfflineSearchEngine and OfflineRoutingEngine to access map data offline. Their counterparts, the SearchEngine and RoutingEngine, will only provide results when an online connection is available - otherwise you will get an error. Therefore, you need to decide which engine to use.

Overview

Downloading and using offline maps can be achieved in two simple steps.

1) Download a list of Region objects. Optionally, this list can be localized with local region names. Once you have this list, you can pick the RegionId you want to download and pass it as download request to the MapDownloader.

2) Use the MapDownloader to download a single Region or a list of regions. You can also download several regions in parallel. Show the download progress to the user by setting a DownloadRegionsStatusListener.

Once the download has completed, the map is ready to be used. If a device is offline, it will automatically show the downloaded region when the camera's target is pointed to that region.

Note

If a download failed, the HERE SDK will still be in a fully operable state. Just try to download again, until the progress has reached 100% and the status finally indicated that the operation has completed. Note that, currently, map data can only be deleted manually from the device's storage.

Since map data for regions, countries or whole continents can contain several hundreds of megabytes, a download may take a while - depending on factors such as available bandwidth. A download may also fail, when a connection gets timed out and cannot recover. For best user experience, it is recommended to allow the user to cancel ongoing operations and to watch the progress until a map download succeeds.

Note

To get a quick overview of how all of this works, you can take a look at the OfflineMapsExample class. It contains all code snippets shown below and it is part of the OfflineMaps example app you can find on GitHub.

Create a MapDownloader Instance

You can create the MapDownloader once per SDKNativeEngine:

SDKNativeEngine sdkNativeEngine = SDKNativeEngine.getSharedInstance();
if (sdkNativeEngine == null) {
    throw new RuntimeException("SDKNativeEngine not initialized.");
}

mapDownloader = MapDownloader.fromEngine(sdkNativeEngine);

Usually, the SDKNativeEngine is automatically initialized when you start the app to show a MapView. Therefore, you can access it's instance at runtime and obtain the MapDownloader from it.

By default, the downloaded map data will be stored to a default location:

// Note that the default storage path can be adapted when creating a new SDKNativeEngine.
String storagePath = SDKNativeEngine.getSharedInstance().getOptions().cachePath;
Log.d("",  "StoragePath: " + storagePath);

As stated in the comment, you can change that storage location, if you wish so - but then you need to create a new SDKNativeEngine instance as shown in the Key Concepts section and set the new cache path together with your credentials as part of the SDKOptions. Note that the storage path is unique for your credentials key.

Download a List of Regions

Each downloadable Region is identified by a unique RegionId. In order to know, which regions are available and which RegionID belongs to which Region, you need to download a list of all available offline maps. This contains regions from the entire world.

Note

Each Region can contain multiple children and each child represents a subset of its parent Region - when you download the parent, then the child regions are automatically included. If you are interested only in smaller portions of an area you can traverse the child regions. Usually, the top-level regions represent continents with countries as children. For the sake of simplicity, below we only look for downloadable countries and ignore any children of children and their children (and so on).

The code below downloads the list of downloadable regions and stores the available Region elements in a list for later use:

// Download a list of Region items that will tell us what map regions are available for later download.
mapDownloader.getDownloadableRegions(LanguageCode.DE_DE, new DownloadableRegionsCallback() {
    @Override
    public void onCompleted(@Nullable MapLoaderError mapLoaderError, @Nullable List<Region> list) {
        if (mapLoaderError != null) {
            String message = "Downloadable regions error: " + mapLoaderError;
            snackbar.setText(message).show();
            return;
        }

        // If error is null, it is guaranteed that the list will not be null.
        downloadableRegions = list;

        for (Region region : downloadableRegions) {
            Log.d("RegionsCallback", region.name);
            List<Region> childRegions = region.childRegions;
            if (childRegions == null) {
                continue;
            }

            // Note that this code ignores to list the children of the children (and so on).
            for (Region childRegion : childRegions) {
                long sizeOnDiskInMB = childRegion.sizeOnDiskInBytes / (1024 * 1024);
                String logMessage = "Child region: " + childRegion.name +
                        ", ID: "+ childRegion.regionId.id +
                        ", Size: " + sizeOnDiskInMB + " MB";
                Log.d("RegionsCallback", logMessage);
            }
        }

        String message = "Found " + downloadableRegions.size() +
                " continents with various countries. See log for details.";
        snackbar.setText(message).show();
    }
});

Note

The response contains either an error or a result: MapLoaderError and List<Region> can never be null at the same time - or non-null at the same time.

Each region can contain child regions. For example, Europe contains Germany, France and Switzerland - and many more child regions. The sizeOnDiskInBytes parameter tells you how much space the downloaded map will occupy on the device's file system when it's uncompressed after download has completed. It makes sense to show this to the user before starting the download - as the available space on a device may be limited.

Screenshot: Showing an example how downloadable maps could be indicated to users.

Download a Region

Once you know the RegionId, you can use it to start downloading the map data. Each Region instance contains a localized name and other data, such as the size of the downloaded map. When the map data is downloaded, all data is compressed and will be unpacked automatically onto the device's disk once the download is complete.

Below we search the downloaded list of regions to find the Region element for Switzerland. Note that we have requested the region list to be localized in German in the step above:

// Finds a region in the downloaded region list.
// Note that we ignore children of children (and so on).
private Region findRegion(String localizedRegionName) {
    Region downloadableRegion = null;
    for (Region region : downloadableRegions) {
        if (region.name.equals(localizedRegionName)) {
            downloadableRegion = region;
            break;
        }
        List<Region> childRegions = region.childRegions;
        if (childRegions == null) {
            continue;
        }
        for (Region childRegion : childRegions) {
            if (childRegion.name.equals(localizedRegionName)) {
                downloadableRegion = childRegion;
                break;
            }
        }
    }

    return downloadableRegion;
}

Once we know the Region, we can use it's RegionId to start the download. We pass the unique ID into a list, so we can download multiple regions with the same request. Here, we download only one region:

// Find region for Switzerland using the German name as identifier.
// Note that we requested the list of regions in German above.
String swizNameInGerman = "Schweiz";
Region region = findRegion(swizNameInGerman);

if (region == null ) {
    String message = "Error: The Swiz region was not found. Click 'Regions' first.";
    snackbar.setText(message).show();
    return;
}

// For this example we only download one country.
List<RegionId> regionIDs = Collections.singletonList(region.regionId);
MapDownloaderTask mapDownloaderTask = mapDownloader.downloadRegions(regionIDs,
        new DownloadRegionsStatusListener() {
            @Override
            public void onDownloadRegionsComplete(@Nullable MapLoaderError mapLoaderError, @Nullable List<RegionId> list) {
                if (mapLoaderError != null) {
                    String message = "Download regions completion error: " + mapLoaderError;
                    snackbar.setText(message).show();
                    return;
                }

                // If error is null, it is guaranteed that the list will not be null.
                // For this example we downloaded only one hardcoded region.
                String message = "Completed 100% for Switzerland! ID: " + list.get(0).id;
                snackbar.setText(message).show();
            }

            @Override
            public void onProgress(@NonNull RegionId regionId, int percentage) {
                String message = "Download for Switzerland. ID: " + regionId.id +
                    ". Progress: " + percentage + "%.";
                snackbar.setText(message).show();
            }

            @Override
            public void onPause(@Nullable MapLoaderError mapLoaderError) {
                if (mapLoaderError == null) {
                    String message = "The download was paused by the user calling mapDownloaderTask.pause().";
                    snackbar.setText(message).show();
                } else {
                    String message = "Download regions onPause error. The task tried to often to retry the download: " + mapLoaderError;
                    snackbar.setText(message).show();
                }
            }

            @Override
            public void onResume() {
                String message = "A previously paused download has been resumed.";
                snackbar.setText(message).show();
            }            
        });

mapDownloaderTasks.add(mapDownloaderTask);

The DownloadRegionsStatusListener provides four events. The second one tells us the progress while the download is ongoing, while the first one notifies once the download has completed. Note that the download can also complete with a MapLoaderError, so it's worth to check if something went wrong.

Note

The response for onDownloadRegionsComplete() contains either an error or a result: MapLoaderError and List<RegionId> can never be null at the same time - or non-null at the same time.

The pause event notfies when a download was paused by the user or the task itself. Internally, the HERE SDK will retry to download a region when it was interrupted, ie. due to a bad network connection. If this happens too often, the MapLoaderError for onPause() is populated and the download pauses. A paused MapDownloaderTask can only be resumed by the user, which will be also indicated by the related event. Especially for larger regions it may be convenient to pause a download until the connection gets better, for example. When resumed, the download will continue at the progress where it stopped and no already downloaded map data will be lost. Note that calling downloadRegions() for a paused region will have the same effect as calling resume() on the original task and the progress will continue where it left off.

After kicking off the download, we get an immediate return value to be able to cancel the ongoing asynchronous download operation. Above, we store the MapDownloaderTask into a list, as a user might trigger the above code multiple times.

To cancel all ongoing downloads, you can use the following code snippet:

public void onCancelMapDownloadClicked() {
    for (MapDownloaderTask mapDownloaderTask : mapDownloaderTasks) {
        mapDownloaderTask.cancel();
    }
    String message = "Cancelled " + mapDownloaderTasks.size() + " download tasks in list.";
    snackbar.setText(message).show();
    mapDownloaderTasks.clear();
}

Note that a MapDownloaderTask that was cancelled cannot be resumed again, but you can start a fresh download request again.

Note

You can find the OfflineMaps example app on GitHub.

Update Downloaded Maps

With the MapUpdater class you can check if newer versions for the downloaded regions you have already installed are available. If one or more update is available, you can download and install the updated map data for all regions by calling performMapUpdate(). An example for this is shown in the OfflineMaps example app on GitHub.

Repair Broken Maps

It is not recommended to keep downloading or updating map data while an app is running in background. However, it may happen that an app gets closed before a map update operation can be completed - for example, due to a crash. So, in worst case an intermediate state may occur on the disk of the device.

The HERE SDK provides a convenient way to check for such issues with the getInitialPersistentMapStatus() method. It also allows to repair a broken map - if possible.

private void checkInstallationStatus() {
    // Note that this value will not change during the lifetime of an app.
    PersistentMapStatus persistentMapStatus = mapDownloader.getInitialPersistentMapStatus();
    if (persistentMapStatus != PersistentMapStatus.OK) {
        // Something went wrong after the app was closed the last time. It seems the offline map data is
        // corrupted. This can eventually happen, when an ongoing map download was interrupted due to a crash.
        Log.d("PersistentMapStatus", "The persistent map data seems to be corrupted. Trying to repair.");

        // Let's try to repair.
        mapDownloader.repairPersistentMap(new RepairPersistentMapCallback() {
            @Override
            public void onCompleted(@Nullable PersistentMapRepairError persistentMapRepairError) {
                if (persistentMapRepairError == null) {
                    Log.d("RepairPersistentMap", "Repair operation completed successfully!");
                    return;
                }

                Log.d("RepairPersistentMap", "Repair operation failed: " + persistentMapRepairError.name());
            }
        });
    }
}

Either way, it is recommended to inform the user that there might be an issue with the downloaded map data. In worst case, the map data needs to be removed and downloaded again.

results matching ""

    No results matching ""