Tutorial: Use HERE Positioning to build a hiking app: A digital diary for your adventure trips

Way to Chandrashilla trek in the Garhwal Himalayan range, 5000 m AMSL

As adventure enthusiasts, we all know the excitement of documenting our hiking and trekking trips to show our friends and family. While traditional methods such as cameras can provide a visual record of these experiences, they do not capture the exact path taken. In this section, we'll use the HERE SDK for Android (Navigate Edition) to record and visualize our audacious trips.

You can use the resulting app as a starting point to create your own customized applications that leverages the HERE SDK's many features and functionalities. Furthermore, the HERE SDK is shipped with readymade code that you can find on GitHub.

Note that our focus will be on recording GPS signals, rather than counting steps because we want to see our hiking path on the HERE map.

Now, let's delve into the use of the HERE SDK to build our HikingDiary app.

Get and show locations

With the code snippets from the HERE SDK found on this GitHub repo, we can easily copy & paste the code to determine our location by integrating the HEREBackgroundPositioningServiceProvider into our app. The HEREBackgroundPositioningServiceProvider uses HEREBackgroundPositioningService as a background service listener to listen to positioning updates even in the background.

While hiking, we usually put our device into the pocket and leave it there. Therefore, to continue tracking our hike, we enable background updates for the app by default.

TheHEREBackgroundPositioningService utilizes the LocationEngine provided by the HERE SDK that uses the Android platform positioning, and provides high-precision location data from a variety of sources such as GPS, network location providers, other Global Navigation Satellite System (GNSS) receivers.

As a next step, we can integrate the copied HEREBackgroundPositioningServiceProvider by integrating the LocationListener in our main class, we named it HikingApp.java, and create a HEREBackgroundPositioningServiceProvider instance in that main class to get started:

// A class to receive location events from the device.
public HEREBackgroundPositioningServiceProvider hereBackgroundPositioningServiceProvider;

// Sets listener to receive locations from HERE Positioning.
hereBackgroundPositioningServiceProvider = new HEREBackgroundPositioningServiceProvider(activity, locationListener);

...

// Receives location updates from LocationListener.
func onLocationUpdated(_ location: heresdk.Location) {
  ...
}

Note: LocationListener is invoked on the main thread and called each time a new Location is available.

Do not forget to add the service class HEREBackgroundPositioningService in the AndroidManifest.xml file.

  <service
    android:name="com.here.hikingdiary.backgroundpositioning.HEREBackgroundPositioningService"
    android:exported="false"/>

Note: You can set the accuracy precision of the location updates via accuracy parameter. The navigation setting is the highest precision possible.

You can find the final implementation of our HikingApp class on GitHub.

Add the required permissions

Location tracking raises some privacy concerns, as it may reveal sensitive information about the user's whereabouts and activities. To address these concerns, many mobile platforms require apps to obtain explicit user consent before accessing location data and provide options to control the frequency and granularity of the location tracking. It is important for app developers to handle location data responsibly and in accordance with the privacy regulations and best practices. Hence, before you start using the LocationEngine in your app, you 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.CHANGE_NETWORK_STATE" />
    <uses-permission android:name="android.permission.CHANGE_WIFI_STATE" />
    <uses-permission android:name="android.permission.INTERNET"/>

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

You can find the resulting AndroidManifest.xml file on GitHub.

Handle location accuracy

Now that we are able to receive locations, we have to check the accuracy of the location:

The Location object that we receive from onLocationUpdated describes the location of the user in the world at a given time and it is just an estimation of the true geographic coordinates. Even if you yourself are stationary, the GNSS satellites are not stationary but travel at a huge speed in the space, which may cause the location estimate to move around. For example, when meditating or having a meal while trekking, the location estimate will vary around my actual location. In such scenarios, the app should not update positions because this way you can travel miles even sitting at home.

Below illustration visualizes the problem with the fluctuation of the received GPS signals:

Illustration: GPS signal visualization.

The hiking man denotes the location of the user (moving or non-moving). The circle surrounding the man is the Accuracy Circle whose radius is defined by horizontalAccuracyInMeters at any time t1. The blue circles are examples of possible locations at any time t1 with 68% probability of lying inside the horizontal accuracy circle, and 32% probability of lying outside the accuracy circle. For example, if the horizontalAccuracyInMeters is 10 meters then it is 68% likely that the true geographic coordinates can lie within the radius of 10 meters. As the hiking man transitions from time t1 to t2, the horizontalAccuracyInMeters value decreases, resulting in a reduced accuracy circle. However, the probability of finding the true geographical coordinates within the accuracy circle increases. Hence, we can conclude that smaller accuracy circles have better accuracy than the bigger one.

With a naive implementation, we could just accept the locations that are below a certain accuracy threshold.

This can lead to imprecise distance calculation, as illustrated below:

Illustration: Problems with distance calculation.

The black lines are the calculated distances based on the GPS signals and the blue lines are the actual distances that are impossible to determine, unless we roll out a rope behind us and track the real path.

How to solve this problem?

Create location filtering algorithm

To solve the above problems, we need to create a location filtering algorithm that accepts locations only when moving. For this, we will create a new DistanceAccuracyLocationFilter class and provide a filtering strategy based on the horizontal accuracy of the location estimate and the distance to our previous location estimate.

For this, we need two types of filters:

  1. DistanceFilter: Filters the locations based on a distance threshold.
  2. AccuracyFilter: Filters the locations based on the horizontal accuracy of the location estimate, and only includes the readings with a certain level of accuracy. To create this filter we will use the horizontalAccuracyInMeters property which is found in the Location object provided by the HERE SDK. It gives the estimated horizontal accuracy in meters and tells us that the true geographic coordinates will lie within this radius of uncertainty with a probability of 68% (see first illustration above).

Let's look how we can implement such a distance filter. Firstly, we need to define a distanceThresholdInMeters property, let's say 15 meters. We will only accept the GPS signal if the estimated location is beyond the distanceThresholdInMeters from the last accepted location. Below illustration visualizes the idea of our planned mechanism:

Illustration: Distance threshold visualization.

Each circle represents a single location estimate with a varying accuracy circle. The crossed circles are below the distance threshold and therefore discarded.

On top of the distance filter, we also need to consider the accuracy of each GPS signal (see above). For this we define a accuracyRadiusThresholdInMeters property. Let's say it is 10 meters. Therefore, each GPS signal with a higher value than accuracyRadiusThresholdInMeters will be filtered out. In our first illustration above, this would mean that all blue GPS signals are accepted and the red ones are discarded.

Now, we are ready to go for the implementation of our algorithm using the two filter mechanisms:

public interface LocationFilterInterface {
  boolean checkIfLocationCanBeUsed(Location location);
}

public class DistanceAccuracyLocationFilter implements LocationFilterInterface {
  // These two parameters define if incoming location updates are considered to be good enough.
  // In the field, the GPS signal can be very unreliable, so we need to filter out inaccurate signals.
  private static final double ACCURACY_RADIUS_THRESHOLD_IN_METERS = 10.0;
  private static final double DISTANCE_THRESHOLD_IN_METERS = 15.0;
  private GeoCoordinates lastAcceptedGeoCoordinates;

  @Override
  public boolean checkIfLocationCanBeUsed(Location location) {
      if (isAccuracyGoodEnough(location) && isDistanceFarEnough(location)) {
          lastAcceptedGeoCoordinates = location.coordinates;
          return true;
      }
      return false;
  }

  // Checks if the accuracy of the received GPS signal is good enough.
  private boolean isAccuracyGoodEnough(Location location) {
      Double horizontalAccuracyInMeters = location.horizontalAccuracyInMeters;
      if (horizontalAccuracyInMeters == null) {
          return false;
      }

      // If the location lies within the radius of ACCURACY_RADIUS_THRESHOLD_IN_METERS then we accept it.
      if (horizontalAccuracyInMeters <= ACCURACY_RADIUS_THRESHOLD_IN_METERS) {
          return true;
      }
      return false;
  }

  // Checks if last accepted location is farther away than xx meters.
  // If it is, the new location will be accepted.
  // This way we can filter out signals that are caused by a non-moving user.
  private boolean isDistanceFarEnough(Location location) {
      if (lastAcceptedGeoCoordinates == null) {
          // We always accept the first location.
          lastAcceptedGeoCoordinates = location.coordinates;
          return true;
      }

      double distance = location.coordinates.distanceTo(lastAcceptedGeoCoordinates);
      if (distance >= DISTANCE_THRESHOLD_IN_METERS) {
          return true;
      }
      return false;
  }
}

The DistanceAccuracyLocationFilter algorithm can be also found on the HERE SDK GitHub repo.

Now, we can create an instance of DistanceAccuracyLocationFilter and filter out the incoming location signals and update the locations on the map:

private LocationFilterInterface locationFilter;

...

// Filter out undesired location signals.
locationFilter = new DistanceAccuracyLocationFilter();

...

public void onLocationUpdated(@NonNull Location location) {
    if (locationFilter.checkIfLocationCanBeUsed(location)) {
      // Use the location.
    }
}

You can also create your own location filter algorithm by adopting the LocationFilterInterface protocol. For example, you can implement a bypass filter like this:

// The DefaultLocationFilter class implements the LocationFilterInterface and
// allows every location signal to pass in order to visualize the raw GPS signals on the map.
public class DefaultLocationFilter implements LocationFilterInterface {
    @Override
    public boolean checkIfLocationCanBeUsed(Location location) {
        return true;
    }
}

With the LocationFilterInterface you can customize the app to use different algorithms for location filtering without changing the core functionality of the app. Note that our filtering algorithm above is kept as simple as possible, it can be improved as per your needs.

Record location updates

Once we have the location updates, the next step is to record them. For this, we use a GPXTrackWriter code snippet from the HERE SDK GitHub that allows us to create a GPXTrack. This GPXTrack can be stored and loaded with the GPXDocument class. The GPXDocument contains all the GPX tracks and is saved in the GPX file format. Hence, once saved, it can be easily shared with other applications that understand the GPX file format.

GPX is a de facto standard data format for GPS data exchange, including waypoints, tracks, and routes. GPX files contain geographical information in XML format and are easy to process with various software programs and GPS devices.

The HERE SDK can help with GPX data by providing tools for reading, displaying, and manipulating GPX data in a variety of ways.

For recording the location updates, create an instance of GPXManager to manage our GPX operations:

private GPXTrackWriter gpxTrackWriter = new GPXTrackWriter();
private GPXManager gpxManager;

// Create a GPXDocument file with named as myGPXDocument.
gpxManager = new GPXManager("myGPXDocument.gpx", context);

Now, we can write location updates to a GPXTrack inside the onLocationUpdated() method:

// Add location updates to a GPX track.
gpxTrackWriter.onLocationUpdated(location)

Finally, to store the hiking trip, we use this one-liner code that stores the GPX track in a GPXDocument:

// Permanently store the trip on the device.
gpxManager.saveGPXTrack(gpxTrackWriter.getTrack());

In order to load our trips, call:

GPXTrack gpxTrack = gpxManager.getGPXTrack(index);
if (gpxTrack == null) {
    return;
}

Note: You can record and store multiple hiking trips as multiple GPXTrack in a single GPXDocument.

Finalize the app

Now, in order to complete our app, we show the travelled path with a MapPolyline on the map. The implementation of a MapPolylinefollows the Map Items Guide that can be found in the Developer Guide for the HERE SDK. It can be also seen in the HikingApp.java class.

In order to extend the polyline during the trip, we take the latest list of coordinates from the gpxManager instance, since it already contains the latest location updates. With this list we can create a new geoPolyline instance that we set to our existing mapPolyline instance:

// Update the polyline shape that shows the travelled path of the user.
mapPolyline.setGeometry(geoPolyline);

This will immediately extend the polyline that is already shown on the map view.

Note that in this tutorial we did not covered every step of the app development process. To get a more complete picture, take a look at this Get Started guide.

You can find the complete Android project on GitHub.

Below you can see the finished app:

Screenshot: HikingDiary app with SATELLITE map scheme.
Screenshot: HikingDiary app with outdoor raster layer.

The HERE SDK provides various map schemes. By default, in our app we show the SATELLITE map scheme. However, you can change the MapScheme and switch to a different map scheme. To know more about the available MapScheme options, you can refer to the API Reference of the HERE SDK.

Also, in our app, you can enable an outdoor raster layer on top of the mapview by sliding a switch at the top-right corner of the main view. Note that this is a 3rd party raster layer that can be suitable for hiking trips, as it shows height lines and more detailed paths. Here, we use an outdoor map from thunderforest.com. More information about custom raster layers can be found in the Developer Guide for the HERE SDK.

Next steps

There are multiple ways to enhance our little HikingDiary app. Below are some ideas:

  • Use the altitude property of the GeoCoordinates object to include a height profile of your trip.
  • Integrate more map schemes, for example, the HERE SDK offers a TERRAIN scheme that provides hill shading.
  • Store your trips with the overall duration and timestamps where you took rests.
  • Include an export option of the stored GPX file to share it with other applications.

Since we released the app as an open-source project on GitHub, contributions are welcome! Feel free to submit pull requests with bug fixes, new features, and overall improvements.

Happy hiking!

results matching ""

    No results matching ""