GuidesFlutter API ReferencesHERE SDK for Android API referencesHERE SDK for iOS API references
Guides

Alternative options

Offline maps are only available with the Navigate license.

Apart from installing map data as Regions into the persisted storage, you can also prefetch data into the cache without the need to use the MapDownloader.

Prefetch map data

The HERE SDK provides support for route prefetching of map data. This allows to improve the user experience - for example, during turn-by-turn navigation to handle temporary network losses gracefully.

Note that this is not needed if offline maps are already downloaded for the region where a trip takes place. In this case, all map data is already there, and no network connection is needed. Unlike, for example, the dedicated OfflineRoutingEngine, the Navigator or VisualNavigator will decide automatically when it is necessary to fallback to cached data or offline map data. In general, navigation requires map data, even if it is executed headless without showing a map view. The reason for this is that map data needs to be accessed during navigation for map matching and, for example, to notify on certain road attributes like speed limits. This data is taken from the available data on the device - or in case it is not there, it needs to be downloaded during navigation. Therefore, it can be beneficial to prefetch more data in anticipation of the road ahead. Without prefetching, temporary connection losses can be handled less gracefully.

Note

Note that this is a beta release of this feature, so there could be a few bugs and unexpected behaviors. Related APIs may change for new releases without a deprecation process.

The RoutePrefetcher constructor requires a SDKNativeEngine instance as only parameter. You can get it via SDKNativeEngine.getSharedInstance() after the HERE SDK has been initialized.

With the RoutePrefetcher you can download map data in advance. The map data will be loaded into the map cache. Note that the map cache has its own size constraints and may already contain data: the RoutePrefetcher may need to evict old cached data in order to store new map data.

  • It is recommended to prefetch once around the departure location before starting a trip. Create a GeoCircle around the current location, wrap it into a GeoPolygon, and call polygonPrefetcher.prefetch(...) to load the tiles into the map cache:
 double radiusInMeters = 2000.0;
 GeoCircle geoCircle = new GeoCircle(currentGeoCoordinates, radiusInMeters);
 polygonPrefetcher.prefetch(new GeoPolygon(geoCircle), new PrefetchStatusListener() {
     @Override
     public void onProgress(int percentage) {
         messageView.setText("Prefetch progress: " + percentage + "%");
     }

     @Override
     public void onComplete(@Nullable MapLoaderError mapLoaderError) {
         if (mapLoaderError == null) {
             messageView.setText("Prefetch completed successfully");
         } else {
             messageView.setText("Prefetch failed: " + mapLoaderError);
         }
     }
 });

This call prefetches map data around the provided location into the map cache and it ensures, that there is enough map data available when a user starts to follow the route - assuming that the route starts from the current location of the user.

Prefetching runs asynchronously and only starts when a free execution thread is available, so the overall app performance impact stays minimal.

  • After navigation has started, consider to call once routePrefetcher.prefetchAroundRouteOnIntervals​(navigator): It prefetches map data within a corridor along the route that is currently set to the provided Navigator instance. If no route is set, no data will be prefetched. The route corridor defaults to a length of 10 km and a width of 5 km. Map data is prefetched only in discrete intervals. Prefetching starts 1 km before reaching the end of the current corridor. Prefetching happens based on the current map-matched location - as indicated by the RouteProgress event. The first prefetching will start after traveling a distance of 9 km along the route. If a new route is set to the navigator, it is not necessary to call this method again - however, it has also no negative impact when it is called twice or more times.

A RoutePrefetcher requires an SDKNativeEngine instance. Here's how to create it:

RoutePrefetcher routePrefetcher = new RoutePrefetcher(SDKNativeEngine.getSharedInstance());
val routePrefetcher = new RoutePrefetcher(SDKNativeEngine.getSharedInstance()!!)

Below you can find an example how to prefetch a circle area:

private void prefetchMapData(GeoCoordinates currentGeoCoordinates) {
    // Prefetches map data around the provided location with a radius of 2 km into the map cache.
    double radiusInMeters = 2000.0;
    GeoCircle geoCircle = new GeoCircle(currentGeoCoordinates, radiusInMeters);
    polygonPrefetcher.prefetch(new GeoPolygon(geoCircle), new PrefetchStatusListener() {
        @Override
        public void onProgress(int percentage) {
            messageView.setText("Prefetch progress: " + percentage + "%");
        }
        @Override
        public void onComplete(@Nullable MapLoaderError mapLoaderError) {
            if (mapLoaderError == null) {
                messageView.setText("Prefetch completed successfully");
            } else {
                messageView.setText("Prefetch failed: " + mapLoaderError);
                }
            }
        });
}
private fun prefetchMapData(currentGeoCoordinates: GeoCoordinates) {
    // Prefetches map data around the provided location with a radius of 2 km into the map cache.
    val radiusInMeters = 2000.0
    val geoPolygon = GeoPolygon(GeoCircle(currentGeoCoordinates, radiusInMeters))
    val polygonPrefetcher= PolygonPrefetcher(SDKNativeEngine.getSharedInstance()!!)
    polygonPrefetcher.prefetch(geoPolygon, object : PrefetchStatusListener {
    override fun onProgress(progress: Int) {
        Log.d(TAG, "Polygon prefetch progress: $progress %")
    }
    override fun onComplete(error: MapLoaderError?) {
        if (error == null) {
            Log.d(TAG, "Polygon prefetch completed successfully")
         } 
        else {
            Log.d(TAG, "Polygon prefetch failed: ${error.name}")
        }
        }
    })
}

Periodic prefetching while following a route can be done using routePrefetcher.prefetchAroundRouteOnIntervals(...). If no route is set, no data will be prefetched. It can be done as follows:

// Prefetches map data within a corridor along the route that is currently set to the provided Navigator instance.
routePrefetcher.prefetchAroundRouteOnIntervals(visualNavigator);
// Prefetches map data within a corridor along the route that is currently set to the provided Navigator instance.
routePrefetcher.prefetchAroundRouteOnIntervals(visualNavigator)

To stop periodic prefetching, use the following:

routePrefetcher.stopPrefetchAroundRoute();
routePrefetcher.stopPrefetchAroundRoute()

If the RoutePrefetcher was successfully used at the start of a route - and then later the connectivity is lost, the cached data will be preserved even across future power cycles until the map cache is evicted. More about the map cache's eviction policy can be found here.

  • For convenience, you can alternatively call both methods together before starting navigation. However, as a trade-off, there might not be enough time to prefetch all required data when the trip starts soon thereafter.
  • Keep in mind that prefetchAroundRouteOnIntervals() increases network traffic continuously during guidance.

Of course, guidance will be also possible without any prefetched data, but the experience may be less optimized:

Both calls help to optimize temporary offline use cases that rely on cached map data. While the prefetchAroundLocationWithRadius() can be also used outside of a navigation use case, prefetchAroundRouteOnIntervals() requires an ongoing navigation scenario.

Alternatively, you can prefetch map data for the entire route in advance. Use RoutePrefetcher.prefetchGeoCorridor() to prefetch tile data for a GeoCorridor created from the route's shape. Since this can take a little longer - depending on the length of the route, the corridor's width and the network - a progress is reported via PrefetchStatusListener.onProgress(). Once the operation is completed, an PrefetchStatusListener.onComplete() event is sent.

Note

In case the route passes already downloaded Region data, then these parts of the corridor are reused and not downloaded again.
Similarly, prefetching will not download data twice that is already available in the map cache. In general, all prefetched data is not stored permanently on the device and may be evicted when newer data is loaded at a later point in time. Also, the available space depends on the map cache size, which can be configured by the user. If not enough space is available, a MapLoaderError will be issued. More information on the map cache can be found here.

Another way to prefetch map data can implemented with the PolygonPrefetcher. This allows to fetch data independent from a concrete Route. Instead only a GeoPolygon is required to indicate where to download the data. Note that this data is only stored temporarily in the map cache - same as for the RoutePrefetcher. The PolygonPrefetcher also allows to estimate the expected downloaded MapDataSize prior to downloading the data.

Monitor the progress of prefetching

The PrefetchStatusListener interface provides callbacks to monitor the progress and completion status of prefetching operations. This is particularly useful when prefetching large areas, as it allows you to provide user feedback and handle completion events appropriately.

The interface defines two methods:

  • onProgress(int percentage): Called multiple times during the prefetch operation to report download progress as a percentage (0-100%).
  • onComplete(MapLoaderError error): Called when the prefetch operation completes, either successfully (error is null) or with an error.

Here's an example of implementing the PrefetchStatusListener when prefetching a circular area:

private void prefetchMapDataWithProgress(GeoCoordinates currentGeoCoordinates) {
    double radiusInMeters = 12000.0;

    GeoCircle geoCircle = new GeoCircle(currentGeoCoordinates, radiusInMeters);
    polygonPrefetcher.prefetch(new GeoPolygon(geoCircle), new PrefetchStatusListener() {
        @Override
        public void onProgress(int percentage) {
            messageView.setText("Prefetch progress: " + percentage + "%");
        }

        @Override
        public void onComplete(@Nullable MapLoaderError mapLoaderError) {
            if (mapLoaderError == null) {
                messageView.setText("Prefetch completed successfully");
            } else {
                messageView.setText("Prefetch failed: " + mapLoaderError);
            }
        }
    });
}

Both callback methods are invoked on the main thread, making it safe to update UI elements directly within these callbacks. The PrefetchStatusListener is available when using operations that support progress reporting, such as PolygonPrefetcher.prefetch() and RoutePrefetcher.prefetchGeoCorridor().

For more details, please refer to the Navigation example app, available for both Java and Kotlin.

Download an area

Instead of downloading a predefined Region, alternatively, you can specify a custom area to download. This area can be set as a polygon or, for example, as a GeoBox, as shown below.

The HERE SDK optimizes storage by saving map data only once, even if the area has been previously downloaded as part of another area or Region.

The DownloadRegionsStatusListener used for downloading Region data is also used when downloading an area. It provides updates on the installation progress in the same way. When downloading an area has been completed, the list contains only one unique RegionId identifying the area. It's recommended to store this ID with a human readable name, as this will make it easier to delete the downloaded area in the future by calling mapDownloader.deleteRegions(...). You can access the ID of a downloaded area through InstalledRegions.

Note

You can call mapDownloader.downloadArea(...) in parallel to download multiple areas simultaneously.

Below is an example of how to persistently install the area currently visible in the viewport:

public void onDownloadAreaClicked() {
    showDialog("Note", "Downloading the area that is currently visible in the viewport.");
    GeoPolygon polygonArea = new GeoPolygon(getMapViewGeoBox());

    mapDownloader.downloadArea(polygonArea, new DownloadRegionsStatusListener() {
        @Override
        public void onDownloadRegionsComplete(@Nullable MapLoaderError mapLoaderError, @Nullable List<RegionId> list) {
            if (mapLoaderError != null) {
                String message = "Download area completion error: " + mapLoaderError;
                snackbar.setText(message).show();
                return;
            }

            // If error is null, it is guaranteed that the regions will not be null.
            String message = "Completed 100% for area! ID: " + list.get(0).id;
            snackbar.setText(message).show();
            Log.d(TAG, message);
        }

        @Override
        public void onProgress(@NonNull RegionId regionId, int percentage) {
            // Note that this ID is uniquely created and can be to delete the area in the future.
            String message = "Download for area ID: " + regionId.id +
                    ". Progress: " + percentage + "%.";
            snackbar.setText(message).show();
        }

        @Override
        public void onPause(@Nullable MapLoaderError mapLoaderError) {
            if (mapLoaderError == null) {
                String message = "The download area was paused by the user calling mapDownloaderTask.pause().";
                snackbar.setText(message).show();
            } else {
                String message = "Download area 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 area download has been resumed.";
            snackbar.setText(message).show();
        }
    });
}
// Download the rectangular area that is currently visible in the viewport.
// It is possible to call this method in parallel to download multiple areas in parallel.
fun onDownloadAreaClicked() {
    showDialog("Note", "Downloading the area that is currently visible in the viewport.")
    val polygonArea = GeoPolygon(getMapViewGeoBox())

    mapDownloader.downloadArea(polygonArea, object : DownloadRegionsStatusListener {
        override fun onDownloadRegionsComplete(
            mapLoaderError: MapLoaderError?,
            list: List<RegionId>?
        ) {
            if (mapLoaderError != null) {
                val message = "Download area completion error: $mapLoaderError"
                snackbar.show(message)
                return
            }

            // If error is null, it is guaranteed that the regions will not be null.
            val message = "Completed 100% for area! ID: " + list!![0].id
            snackbar.show(message)
            Log.d(TAG, message)
        }

        override fun onProgress(regionId: RegionId, percentage: Int) {
            // Note that this ID is uniquely created and can be to delete the area in the future.
            val message = "Download for area ID: " + regionId.id +
                    ". Progress: " + percentage + "%."
            snackbar.show(message)
        }

        override fun onPause(mapLoaderError: MapLoaderError?) {
            if (mapLoaderError == null) {
                val message =
                    "The download area was paused by the user calling mapDownloaderTask.pause()."
                snackbar.show(message)
            } else {
                val message =
                    "Download area onPause error. The task tried to often to retry the download: $mapLoaderError"
                snackbar.show(message)
            }
        }

        override fun onResume() {
            val message = "A previously paused area download has been resumed."
            snackbar.show(message)
        }
    })
}

For this example we use a GeoBox to define the region we want to download. The above code including the code for getMapViewGeoBox() can be found in the accompanying "OfflineMaps" example app, you can find on GitHub.

Get a list of installed regions

Using the MapDownloader, you can retrieve a list of InstalledRegion elements to determine which regions are currently installed on the device. You can also check their installation status. Use region.sizeOnDiskInBytes to calculate the total storage usage, as shown below.

// Get the list of all downloaded regions.
private List<InstalledRegion> getInstalledRegionList() {
    List<InstalledRegion> installedRegionList = new ArrayList<>();
    try {
        installedRegionList = mapDownloader.getInstalledRegions();
    } catch (MapLoaderException e) {
        Log.d("Fetching installedRegions failed", e.error.toString());
    }
    return installedRegionList;
}

// Log all the regions downloaded and total storage usage of downloaded map areas.
private void logInstalledRegions() {
    List<InstalledRegion> installedRegionList = getInstalledRegionList();

    for (InstalledRegion region : installedRegionList) {
        Log.d("Installed region", "Downloaded region id: " + region.regionId);
        Log.d("Installed region", "sizeOnDiskInBytes: " + region.sizeOnDiskInBytes);
        Log.d("Installed region", "InstalledRegionStatus: " + region.status.toString());
    }

    long occupiedStorageSize = getSizeOfInstalledRegionsInBytes(installedRegionList);
    Log.d("Installed Region",  "Total storage size in bytes: " + occupiedStorageSize);  
}

private long getSizeOfInstalledRegionsInBytes(List<InstalledRegion> installedRegionList) {
    return installedRegionList.stream()
            .mapToLong(region -> region.sizeOnDiskInBytes)
            .sum();  
}
private fun getInstalledRegionList(): List<InstalledRegion> {
    var installedRegionList: List<InstalledRegion> = ArrayList()
    try {
        installedRegionList = mapDownloader.getInstalledRegions()
    } catch (e: MapLoaderException) {
        Log.d("Fetching installedRegions failed", e.error.toString())
    }
    return installedRegionList
}

// Log all the regions downloaded and total storage usage of downloaded map areas.
private fun logInstalledRegions() {
    val installedRegionList = getInstalledRegionList()

    for (region in installedRegionList) {
        Log.d("Installed region", "Downloaded region id: " + region.regionId)
        Log.d("Installed region", "sizeOnDiskInBytes: " + region.sizeOnDiskInBytes)
        Log.d("Installed region", "InstalledRegionStatus: " + region.status.toString())
    }

    val occupiedStorageSize = getSizeOfInstalledRegionsInBytes(installedRegionList)
    Log.d("Installed Region", "Total storage size in bytes: $occupiedStorageSize")
}


private fun getSizeOfInstalledRegionsInBytes(installedRegionList: List<InstalledRegion>): Long {
    return installedRegionList.stream()
        .mapToLong { region: InstalledRegion -> region.sizeOnDiskInBytes }
        .sum()
}