Find your Location

One of the main reasons to use a mapping application is to find out where you are.

The LocationEngine provided by the HERE SDK implements a comprehensive location solution that works with several location sources such as GPS or other Global Navigation Satellite System (GNSS) receivers, mobile network signals and Wi-Fi network signals to determine accurate locations.

At a glance

Integrating the HERE SDK location features requires at least the following steps:

  1. Add the required Android permissions to your manifest file and request the permissions from the user.
  2. Create a ConsentEngine and show a consent dialog whether data can be collected by the LocationEngine or not.
  3. Show the outcome of the consent dialog and allow the user to revoke a previous decision.
  4. Create a LocationEngine and set at least one LocationListener.
  5. Start the LocationEngine once and set the desired accuracy level.
  6. Receive Location updates and handle them in your app.

Add the Required Permissions

Before you can start using the LocationEngine in your app, you will need to add the required permissions to the app's AndroidManifest.xml file:

...
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
<uses-permission android:name="android.permission.ACCESS_WIFI_STATE"/>
<uses-permission android:name="android.permission.ACTIVITY_RECOGNITION" />
<uses-permission android:name="android.permission.CHANGE_NETWORK_STATE" />
<uses-permission android:name="android.permission.CHANGE_WIFI_STATE" />
<uses-permission android:name="android.permission.READ_PHONE_STATE" android:maxSdkVersion="22" />
<uses-permission android:name="android.permission.WAKE_LOCK" />

If your application targets SDK version 23 or higher, you can leave out the READ_PHONE_STATE permission. If your application targets SDK version 28 or lower, you can leave out the ACTIVITY_RECOGNITION permission.

For all example apps accompanying this user guide, we use a convenience class called PermissionsRequestor which takes care of the burdening task to request the user's permission.

An app using native location services such as GPS will ask for the user's permission. Not all devices provide the same capabilities and may have certain hardware restrictions that can lead to varying results.

Prior to using the LocationEngine, it may be a good idea to check if the native location services are enabled. On most Android devices a user can decide to turn the location services on, and even increase the accuracy, by opening the device's Settings and navigating to the Security & location section.

The LocationEngine contains functionality that can gather certain information about the surroundings of the mobile device in order to improve the HERE services used by the HERE SDK. An example of such information is the strength of the nearby Wi-Fi and mobile network signals.

The HERE SDK provides a ConsentEngine that handles the flow to acquire the user’s consent to collect such data. In addition, it allows to retrieve the current status and to revoke a previous decision whether to collect data or not. The application must ensure that this is accessible for the user at all times.

Note: Requirement

Showing a consent dialog is mandatory. The LocationEngine will not deliver location data to the app until the user has made a decision. Note that the LocationEngine will be fully operable regardless if the consent is declined by a user or not.

Two steps are required:

  • Show the consent dialog by calling consentEngine.requestUserConsent().
  • Your application must ensure to show the user's current decision and to revoke a previous decision: Get the current decision via consentEngine.getUserConsentState(). Revoke a previous decision by allowing the user to call requestUserConsent() again. This must be possible at any time during the lifecycle of your app.

These steps are explained in greater detail below. See the HERE SDK Privacy Supplement for more information. Note that information is only collected if the user has given their consent. Gathered information does not identify the user, and HERE will only retain anonymized information.

Before starting the LocationEngine, you need to ensure that the user's consent to collect the before mentioned information has been handled. It does not matter what the answer was (if the user accepted the collection of data or not), only that they were shown the consent dialog and that an answer was given. The LocationEngine will return LocationEngineStatus.USER_CONSENT_NOT_HANDLED status when attempting to start it without having handled the user's consent.

The code snippet below creates an instance of the ConsentEngine, checks if the user’s consent has already been requested and if not, invokes a UI dialog (pictured also below) which explains the details of information gathering to the user, and provides them with the possibility to either grant or deny permission.

try {
    consentEngine = new ConsentEngine();
} catch (InstantiationErrorException e) {
    throw new RuntimeException("Initialization of ConsentEngine failed: " + e.getMessage());
}

// Check if user consent has been handled.
if (consentEngine.getUserConsentState() == Consent.UserReply.NOT_HANDLED) {

    // Show dialog.
    consentEngine.requestUserConsent();
}

// The execution can continue while the dialog is being shown.

Screenshot: Consent dialog.

The dialog contains a link to a web page describing the privacy practices of HERE and supports 37 languages. When shown, the dialog will be displayed according to the device's language preferences, or in English, if they are not supported.

The user's response persists between the usage sessions of the application and can be retrieved using the getUserConsentState() method, which returns a Consent.UserReply value:

switch(consentEngine.getUserConsentState()) {
    case GRANTED:
        //The user has previously given permission.
        consentStateTextView.setText(R.string.consent_state_granted);
        break;
    case DENIED:
        // The user has previously denied permission.
    case NOT_HANDLED:
        //The user has not been asked for consent.
    case REQUESTING:
        //The dialog is currently being shown to the user.
        consentStateTextView.setText(R.string.consent_state_denied);
        break;
    default:
        throw new RuntimeException("Unknown consent state.");
}

Keep in mind that the application must provide the possibility for the user to see what response they have given earlier, by calling the getUserConsentState() method. Above we use the consentStateTextView to show the current state. In addition, the application must also make it possible for the user to change their mind on the consent at any time, by calling the requestUserConsent() method and displaying the consent dialog again.

When requestUserConsent() is called the HERE SDK shows a new Activity containing the dialog. When the dialog is dismissed, the previous Activity is resumed. If a MapView is shown before the dialog is opened, the MapView will be paused until it is shown again.

An example flow can be seen in the Positioning example app you can find on GitHub.

It is possible to customize the look and the content of the dialog. For this, ask HERE to certify the application’s own customized user consent dialog. Once the certification is received the application can use the methods grantUserConsent() and denyUserConsent() to communicate the user’s response to the HERE SDK.

Learn more about this option or initiate the certification process by contacting HERE via your HERE Sales representative or via our help page.

Create a LocationEngine

Creating a new LocationEngine is simple:

try {
    locationEngine = new LocationEngine();
} catch (InstantiationErrorException e) {
    throw new RuntimeException("Initialization of LocationEngine failed: " + e.getMessage());
}

It is not possible to initialize the LocationEngine during the Application's onCreate() lifecycle. Any other point in time is fine. For example, a good place to initialize the engine may be in an Activity's onCreate()-method.

Get the Last Known Location

Once the engine is initialized, the last known location can be obtained, as long as the engine has been started at least once before and received at least one position, otherwise null will be returned. This information will remain, so the last known location will also be available between application sessions.

Location myLastLocation = locationEngine.getLastKnownLocation();

if (myLastLocation != null) {
    // Log the last known location coordinates.
    Log.d(TAG, "Last known location: " + myLastLocation.coordinates.latitude + ", " + myLastLocation.coordinates.longitude);
}

Note that the LocationEngine does not need to be started nor any listener needs to be set in order to get the last known location. It is enough that the LocationEngine was successfully started once in a previous session and that a valid location event was received at least once. The Location object contains a timestamp that indicates when that location was received.

Get Notified on Location Events

Next before starting the LocationEngine, it's a good idea to register a LocationStatusListener so that you will be notified of changes in the engine's status. To do so, implement the LocationStatusListener interface and register it with the location engine's addLocationStatusListener() method. Check the API Reference for more information on the different statuses.

private final LocationStatusListener locationStatusListener = new LocationStatusListener() {
    @Override
    public void onStatusChanged(@NonNull LocationEngineStatus locationEngineStatus) {
        Log.d(TAG, "LocationEngineStatus: " + locationEngineStatus.name());
    }

    @Override
    public void onFeaturesNotAvailable(@NonNull List<LocationFeature> features) {
        for (LocationFeature feature : features) {
            Log.d(TAG, "Feature not available: " + feature.name());
        }
    }
};

// ...

// Add the listener.
locationEngine.addLocationStatusListener(locationStatusListener);

After a successful start, LocationStatusListener will always receive status LocationEngineStatus.ENGINE_STARTED, and after a successful stop, it will always receive status LocationEngineStatus.ENGINE_STOPPED.

Additionally, through the listener's onFeaturesNotAvailable() callback you will be notified of any LocationFeature that is not available. If a feature that you need is not available, contact your HERE representative. Note: LocationFeature enum is currently a pending feature.

The last thing to consider before starting the engine is registering a LocationListener, which provides the onLocationUpdated() callback that sends a notification once a new Location is detected. You can do so in a similar way as with the previously mentioned LocationStatusListener:

private final LocationListener locationListener = new LocationListener() {
    @Override
    public void onLocationUpdated(@NonNull Location location) {
        Log.d(TAG, "Received location: " + location.coordinates.latitude + ", " + location.coordinates.longitude);
    }
};

// ...

// Add the listener.
locationEngine.addLocationListener(locationListener);

The callback onLocationUpdated() is received on the main thread - same as for all other callbacks.

Apart from the current geographic coordinates, the Location instance may contain many more useful information, such as the current altitude, bearing, speed, accuracy and more. See the Access Accuracy Information from a Location section below for more information.

You can add as many LocationStatusListener and LocationListener as you need by calling the respective addLocationStatusListener() and addLocationListener() methods.

Start and Stop Receiving Locations

You are now ready to call the LocationEngine's start() method:

try {
    consentEngine = new ConsentEngine();
    locationEngine = new LocationEngine();
} catch (InstantiationErrorException e) {
    throw new RuntimeException("Initialization failed: " + e.getMessage());
}

if (consentEngine.getUserConsentState() == Consent.UserReply.NOT_HANDLED) {
    consentEngine.requestUserConsent();
}

// ...

startLocating();

// ...

private void startLocating() {
    locationEngine.addLocationStatusListener(locationStatusListener);
    locationEngine.addLocationListener(locationListener);
    locationEngine.start(LocationAccuracy.BEST_AVAILABLE);
}

The most straightforward way to start the engine is by passing it one of the pre-defined LocationAccuracy modes, as in the code snippet above. See the table below or check the API Reference for more information about all the available modes.

After the LocationEngine has been started, you will receive LocationEngineStatus.ALREADY_STARTED if you try to start it again without calling stop() first. You can use the method isStarted() to check if the engine is started or not. Similarly, if you have started a LocationEngine and try to start another one without stopping the first, you will get LocationEngineStatus.ALREADY_STARTED error. Only one engine can be started at a time.

If you don't want to receive more location updates, you can stop the engine by calling the stop() method. Remember to remove the listeners when they are no longer needed:

public void stopLocating() {
    locationEngine.stop();
}

// ...

locationEngine.removeLocationListener(locationListener);
locationEngine.removeLocationStatusListener(locationStatusListener);

Enable Background Updates

If you target Android 10 (API level 29) or higher and you want to continue receiving location updates while the application is running in the background, you need to enable such capability by adding the following permission to the app's AndroidManifest.xml file:

<uses-permission android:name="android.permission.ACCESS_BACKGROUND_LOCATION" />

Additionally the user needs to be requested for autorization. Check the Add the Required Permissions section a suggestion on how to request permissions from the user.

If your application targets SDK version 28 or lower, as long as your app already requests for the permissions mentioned in the earlier Add the Required Permissions section, you don't need to make any changes to support background updates.

Specify Location Options

If you want more control over what options are taken into account when generating the locations, you can create a LocationOptions object, configure it to your liking, and start the engine with it.

// ...

// Create a new LocationOptions object. By default all options are enabled.
LocationOptions locationOptions = new LocationOptions();

// Use WiFi and satellite (GNSS) positioning only.
locationOptions.wifiPositioningOptions.enabled = true
locationOptions.satellitePositioningOptions.enabled = true
locationOptions.sensorOptions.enabled = false;
locationOptions.cellularPositioningOptions.enabled = false

// Receive a location approximately every minute, but not more often than every 30 seconds.
locationOptions.notificationOptions.smallestIntervalMilliseconds = TimeUnit.SECONDS.toMillis(30);
locationOptions.notificationOptions.desiredIntervalMilliseconds = TimeUnit.SECONDS.toMillis(60);

locationEngine.start(locationOptions);

// ...

The table below shows an overview of the available LocationAccuracy modes, and how they are internally translated to LocationOptions:

LocationAccuracy LocationOptions
BEST_AVAILABLE cellularPositioningOptions.enabled = true
satellitePositioningOptions.enabled = true
wifiPositioningOptions.enabled = true
sensorOptions.enabled = true
notificationOptions.desired_interval_millisec = 30000 (30s)
notificationOptions.smallest_interval_millisec = 1000 (1s)
NAVIGATION cellularPositioningOptions.enabled = false
satellitePositioningOptions.enabled = true
wifiPositioningOptions.enabled = true
sensorOptions.enabled = true
notificationOptions.desired_interval_millisec = 1000 (1s)
notificationOptions.smallest_interval_millisec = 1000 (1s)
TENS_OF_METERS cellularPositioningOptions.enabled = false
satellitePositioningOptions.enabled = false
wifiPositioningOptions.enabled = true
sensorOptions.enabled = true
notificationOptions.desired_interval_millisec = 30000 (30s)
notificationOptions.smallest_interval_millisec = 1000 (1s)
HUNDREDS_OF_METERS cellularPositioningOptions.enabled = true
satellitePositioningOptions.enabled = false
wifiPositioningOptions.enabled = true
sensorOptions.enabled = false
notificationOptions.desired_interval_millisec = 30000 (30s)
notificationOptions.smallest_interval_millisec = 1000 (1s)
KILOMETERS cellularPositioningOptions.enabled = true
satellitePositioningOptions.enabled = false
wifiPositioningOptions.enabled = false
sensorOptions.enabled = false
notificationOptions.desired_interval_millisec = 30000 (30s)
notificationOptions.smallest_interval_millisec = 1000 (1s)
Table: Mapping of LocationAccuracy to LocationOptions.

The desired interval is not guaranteed by the LocationEngine, so it is possible that the locations will be delivered more or less often. The smallest interval, on the other hand, guarantees that the locations are not provided more often than the defined value.

Access Accuracy Information from a Location

Except for the coordinates and the timestamp, all other Location fields are optional. For example, the received Location object may contain the information about the bearing angle, as well as the current speed, but this is not guaranteed to be available. Unavailable values will be returned as null. What kind of sources are used for positioning (as defined by the above mentioned LocationOptions), and the device's capabilities affect what fields will be available.

if (location.speedInMetersPerSecond != null) {
    Log.d(TAG, "Speed (m/s): " + location.speedInMetersPerSecond);
} else {
    Log.d(TAG, "Speed (m/s): Not available");
}

The horizontalAccuracyInMeters (or: radius of uncertainty) tells us that the true geographic coordinates lie with a probability of 68% within that radius. This can be used to draw a halo indicator around the current location.

Illustration: Radius of uncertainty.

Similarly for the altitude, a verticalAccuracyInMeters value of 10 meters means that the real altitude lies with a probability of 68% in the range between altitude - 10m and altitude + 10m. Other accuracy values, like bearingAccuracyInDegrees and speedAccuracyInMetersPerSecond will follow the same rule: a smaller uncertainty results in a better accuracy.

On Android devices, the coordinates.altitude value is given in relation to the WGS 84 reference ellipsoid.

Achieving probabilities other than 68% (CEP68)

What if the given probability of 68% (CEP68) is not enough - is it possible to achieve an accuracy of 99%? Yes, it is: Since the given circular error probability (CEP) follows a chi-squared distribution with two degrees-of-freedom, it is easy to calculate the desired probability based on the following formulas:

Probability Radius of Uncertainty
50% CEP50 = 0.78 x CEP68
60% CEP60 = 0.90 x CEP68
70% CEP70 = 1.03 x CEP68
80% CEP80 = 1.19 x CEP68
90% CEP90 = 1.42 x CEP68
95% CEP95 = 1.62 x CEP68
99% CEP99 = 2.01 x CEP68
Table: Formulas to calculate the desired probability.

The table above can be used to visualize various probability levels for a halo indicator on the map. For example, if the horizontal accuracy is 20 meters, you can (roughly) double the radius to achieve a probability of 99%. The accuracy value is always given as CEP68, that means:

CEP99 = 2.01 x CEP68 = 2.01 x 20m = 40.2m

Now you can draw a radius of 40.2 meters around the found location - and with a probability of 99%, the real location will lie within that circle. On the other hand, the probability for a radius of 0 meters is 0%.

Using the HERE SDK location features requires you to show the HERE SDK consent dialog in your application as described above. Users must be able to see their current consent decision and to revoke any previous consent decision - otherwise, you are not allowed to use the HERE SDK location features and you must refer to the Android location APIs instead.

Tutorial: Show your Current Location on a Map

A custom map circle can be composed out of two MapPolygon instances: one representing the radius of uncertainty as a halo, and the other one representing the center. Before the circle is updated with a current location value, a default Location is set, which can be the last known location - or just any place the user should see before the first location update arrives.

// MapPolygon objects to represent a location circle.
private MapPolygon locationAccuracyCircle;
private MapPolygon locationCenterCircle;

// ...

private void addMyLocationToMap(@NonNull GeoCoordinates geoCoordinates, @NonNull double accuracyRadiusInMeters) {
    //Transparent halo around the current location: the true geographic coordinates lie with a probability of 68% within that.
    locationAccuracyCircle = new MapPolygon(new GeoPolygon(new GeoCircle(geoCoordinates, accuracyRadiusInMeters)), colorAccuracy);
    //Solid circle on top of the current location.
    locationCenterCircle = new MapPolygon(new GeoPolygon(new GeoCircle(geoCoordinates, CENTER_RADIUS_IN_METERS)), colorCenter);

    //Add the circle to the map.
    mapView.getMapScene().addMapPolygon(locationAccuracyCircle);
    mapView.getMapScene().addMapPolygon(locationCenterCircle);
}

// ...

private void updateMyLocation(@NonNull GeoCoordinates geoCoordinates, @NonNull double accuracyRadiusInMeters) {
    locationAccuracyCircle.updateGeometry(new GeoPolygon(new GeoCircle(geoCoordinates, accuracyRadiusInMeters)));
    locationCenterCircle.updateGeometry(new GeoPolygon(new GeoCircle(geoCoordinates, CENTER_RADIUS_IN_METERS)));
}

// ...

//Default start-up location.
GeoCoordinates defaultLocation = new GeoCoordinates(52.520798, 13.409408);

Location myLastLocation = locationEngine.getLastKnownLocation();

if (myLastLocation != null) {
    final double accuracy = (myLastLocation.horizontalAccuracyInMeters != null) ? myLastLocation.horizontalAccuracyInMeters: 0.0;

    // Show the obtained last known location on a map.
    addMyLocationToMap(myLastLocation.coordinates, accuracy);

    //Update the map viewport to be centered on the location.
    mapView.getCamera().lookAt(myLastLocation.coordinates, CAMERA_DISTANCE_IN_METERS);
} else {
    // No last known location available, show a pre-defined location.
    addMyLocationToMap(defaultLocation, 0.0);

    //Update the map viewport to be centered on the location.
    mapView.getCamera().lookAt(defaultLocation, CAMERA_DISTANCE_IN_METERS);
}

// ...

private final LocationListener locationListener = new LocationListener() {
    @Override
    public void onLocationUpdated(@NonNull Location location) {
        final double accuracy = (myLastLocation.horizontalAccuracyInMeters != null) ? myLastLocation.horizontalAccuracyInMeters: 0.0;
        updateMyLocation(location.coordinates, accuracy);

        //Update the map viewport to be centered on the location.
        mapView.getCamera().lookAt(location.coordinates, CAMERA_DISTANCE_IN_METERS);
    }
};

Screenshot: Showing a custom map circle indicating current location.

As shown in the implementation above, you can get the GeoCoordinates from the Location object and pass it to the custom map circle that represents your current location. In this example, the goal is to track the user's current location - therefore, the map viewport's center location is updated as well. See Map Items for more information on items such as polylines, polygons and markers you can add to the map.

results matching ""

    No results matching ""