07 – Navigation

Please note! All the code snippets in this section have been provided for Android. If you use iOS, Cordova or ReactNative you’ll find it easy to translate them appropriately.

As we’ve seen in a previous section, Situm SDK is able to compute static routes from point A to B using the DirectionsManager (Android, iOS, Cordova, ReactNative). However, you may be wondering… what if the user advances in the route? what if she goes off-route? do I have to re-compute the route each time?

Luckily, Situm SDK facilitates the dynamic navigation along these routes, receiving route update while the user moves. This will allow us to re-compute the route if the user goes off-route, re-draw the route points while the user advances, re-calculate the remaining distance and estimated time of arrival, knowing when the user has reached the destination, etc. Situm SDK provides utilities to do all this effortlessly.

The central entity in navigation is the NavigationManager (called NavigationInterface in iOS) and its method to “requestNavigationUpdates” (Android, iOS, Cordova, ReactNative). Essentially, this manager expects the application to:

  • Instantiate it with the appropriate parameters: threshold distance before the SDK considers the user has gone off-route, threshold distance to consider that the user has arrived at the destination, etc.
  • Feed it with every location: the SDK will update the navigation progress taking into account each new location.

Provided this, the NavigationManager is able to inform the application about three events as the user moves along the route:

  • Whether the user has reached a destination point.
  • Whether the user has gone off-route.
  • Whether the user has moved (forward or backwards) along the route. If this is the case, the manager will provide a helper data structure that will allow you to easily compute the remaining distance & estimated time, the remaining points & segments, etc.

With these, you’ll be able to compute routes & provide a dynamic navigation experience in your application.

A complete navigation example #

Warning! This example is artificially simple in order to help in understanding the main navigation concepts. In your production app, you should design your own architecture by adhering to industry-standard coding practices.

We will explain Situm navigation by using a simple example. We assume that:

  • The user is in a certain building and we want to guide the user to a specific point in that building,
  • We want to perform positioning only in that building (hence, we’re using Building Mode).
  • For the sake of simplicity in the example’s code, we will only start positioning after we retrieve the building information.

The following code snippet shows the skeleton of our application. This application does the following:

  1. Declares common constants:
    1. Building identifier where the user will geolocate (“10194”)
    2. Target location in the building to which we want to guide the user (floor “29685”, latitude “43.3434333899059” and longitude “-8.42761197887612”)
  2. Declares a variable isNavigating with which we will control whether we’re navigating or not.
  3. Declares a variable buildingInfo to store building information when it’s ready.
  4. Initializes Situm SDK and asks for location permissions.
  5. Retrieves the building information and stores it in the appropriate variable (step 3).
  6. Starts positioning in that building.
  7. When a location is received:
    1. If navigation wasn’t running, starts it from the user’s current location to the target location (see step 1).
    2. Otherwise, updates the NavigationManager with the current location (so the user “advances” on the route).
package ...;

import ...;

public class MainActivity extends AppCompatActivity {
    private static final String TAG = MainActivity.class.getSimpleName();

    // We will geolocate & ask for navigation in a specific building in this example
    private static final String buildingId = "10194";

    // Floor and coordinate to which we will compute a route
    private static final String targetFloorId = "29685";
    private static final Coordinate targetCoordinate =
            new Coordinate(43.3434333899059, -8.42761197887612);

    // This controls whether we are on navigation or not
    private boolean isNavigating = false;
    // To store the building information after we retrieve it
    private BuildingInfo buildingInfo = null;
    //This is only required to use the "toText" method when retrieving indications
    Context myContext = this;

     @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        // Init Situm SDK
        SitumSdk.init(this);

        // We ask for location and BLE permissions
        // Warning! This is a naive way to ask for permissions for the example's simplicity
        // Check https://developer.android.com/training/permissions/requesting to know how
        // to ask for permissions
        ActivityCompat.requestPermissions(MainActivity.this,
                new String[]{
                        Manifest.permission.ACCESS_FINE_LOCATION,
                        Manifest.permission.ACCESS_COARSE_LOCATION,
                        Manifest.permission.BLUETOOTH_SCAN,
                        Manifest.permission.BLUETOOTH_CONNECT}, 0);

        // We retrieve the information first, to make sure that
        // we have all the required info afterwards.
        SitumSdk.communicationManager().fetchBuildingInfo(buildingId, new Handler<BuildingInfo>() {

            @Override
            public void onSuccess(BuildingInfo bInfo) {

                //Store building info
                buildingInfo = bInfo;

                // ... and start positioning in that building
                LocationRequest locationRequest = new LocationRequest.Builder().
                        useWifi(true).useBle(true).
                        buildingIdentifier("10194").
                        build();
                SitumSdk.locationManager().
                        requestLocationUpdates(locationRequest, locationListener);
            }

            @Override
            public void onFailure(Error error) {
                Log.e(TAG, "Error " + error);
            }
        });

    }



    // The LocationListener receives every location, status or error produced by Situm SDK
    // If navigating, we update the NavigationManager with the new location
    // Otherwise, we start the navigation
    private LocationListener locationListener = new LocationListener() {
        @Override
        public void onLocationChanged(@NonNull Location location) {
            Log.i(TAG, "Updating navigation with: location = [" + location + "]");
            if (isNavigating) {
                SitumSdk.navigationManager().updateWithLocation(location);
            }
            else {
                startNavigation(location);
            }
        }
        @Override
        public void onStatusChanged(@NonNull LocationStatus locationStatus) {
            Log.i(TAG, "onStatusChanged() called with: status = [" + locationStatus + "]");
        }
        @Override
        public void onError(@NonNull Error error) {
            Log.e(TAG, "onError() called with: error = [" + error + "]");
        }
    };


    // Computes a route from the current location to the targetCoordinate and
    // starts navigation along that route
    private void startNavigation(Location location){
       ...

    }


    // Receives 3 navigation events when a new location is fed to the NavigationManager
    // onDestinationReached -> The user has arrived to the destination
    // onProgress -> The user has moved along the route
    // onUserOutsideRoute -> The user went off-route
    private NavigationListener navigationListener = new NavigationListener() {
        ...

    };

}

Starting the navigation #

In the previous snippet, we didn’t show the code that actually starts the navigation. The following snippet does this job.

First of all, in order to start the navigation you should first compute a route. The snippet does this first by calling the DirectionsManager (for a detailed explanation, please check the section Routes).

After that, when the route is computed, you’re ready to start the navigation by configuring a NavigationRequest (Android, iOS, Cordova, ReactNative) and calling the method requestNavigationUpdates (Android, iOS, Cordova, ReactNative). You can configure several parameters of the navigation request:

  • Threshold to goal (in meters): when the user is closer to the goal than this threshold, your application will be notified.
  • Threshold to out-of-route (in meters): when the user goes off-route farther away than this distance, your application will be notified (more info).
  • Ignore low quality locations: whether low quality locations will be taken into account when computing navigation updates (more info).
  • Time to ignore unexpected floor changes: if the user goes to a floor that is not expected (e.g. a floor that is not within the route), this establishes a delay until that location is taken into account. This increases the robustness against floor detection errors (more info).
// Computes a route from the current location to the targetCoordinate and
    // starts navigation along that route
    private void startNavigation(Location location){

        // Removes previous navigation (just in case)
        SitumSdk.navigationManager().removeUpdates();

        // First, we build the coordinate converter ...
        CoordinateConverter coordinateConverter = new CoordinateConverter(buildingInfo.getBuilding().getDimensions(), buildingInfo.getBuilding().getCenter(), buildingInfo.getBuilding().getRotation());

        // ... which allows us to build the destination point of the route
        Point pointB = new Point(buildingId, targetFloorId, targetCoordinate, coordinateConverter.toCartesianCoordinate(targetCoordinate));

        // ... while the origin point is just our current location
        Coordinate coordinateA = new Coordinate(location.getCoordinate().getLatitude(), location.getCoordinate().getLongitude());
        Point pointA = new Point(location.getBuildingIdentifier(), location.getFloorIdentifier(), coordinateA, coordinateConverter.toCartesianCoordinate(coordinateA) );

        // The DirectionsRequest allows us to configure the route calculation: source point, destination point, other options...
        DirectionsRequest directionsRequest = new DirectionsRequest.Builder().from(pointA, null).to(pointB).build();

        // Then, we compute the route
        SitumSdk.directionsManager().requestDirections(directionsRequest, new Handler<Route>() {
            @Override
            public void onSuccess(Route route) {

                // When the route is computed, we configure the NavigationRequest
                NavigationRequest navigationRequest = new NavigationRequest.Builder().
                        // Navigation will take place along this route
                        route(route).
                        // ... stopping when we're closer than 10 meters to the destination
                        distanceToGoalThreshold(10).
                        // ... or we're farther away than 10 meters from the route
                        outsideRouteThreshold(10).
                        // Low quality locations will not be taken into account when updating the navigation state
                        ignoreLowQualityLocations(true).
                        // ... neither locations computed at unexpected floors if the user
                        // is less than 1000 consecutive milliseconds in those floors
                        timeToIgnoreUnexpectedFloorChanges(1000).
                        build();

                // We start the navigation with this NavigationRequest,
                // which allows us to receive updates while the user moves along the route
                SitumSdk.navigationManager().requestNavigationUpdates(navigationRequest,  navigationListener);

                isNavigating=true;
                Log.i(TAG, "Navigating!");
            }

            @Override
            public void onFailure(Error error) {
                Log.e(TAG, "Error" + error);
            }
        });

    }

Starting on Android SDK 2.87.0, you can call NavigationRequest#setRoute(Route) so the Route can be attached to the navigation request after the instance is constructed with NavigationRequest.Builder.

From Android SDK 3.1.1, you can configure positioning in navigation if user has selected a route. This setting is used to adjust the user’s position to the route. This settings is configurable through several parameters.

Receiving navigation updates #

Now everything is ready! Provided that Situm SDK is generating valid geolocations, your application will receive 3 possible navigation events:

  • The user has reached the destination point. Usually, at this point you should stop the navigation and maybe show an informative pop-up to the user.
  • The user has gone off-route. Usually, if this happens you should inform the user. You can also re-compute the route and start the navigation process again.
  • The user has moved along the route (within the route limits). This is the most common situation. Usually, you’ll want to re-draw the route in your interface to reflect that the user moved forward (or backward!).
    // Receives 3 navigation events when a new location is fed to the NavigationManager
    // onDestinationReached -> The user has arrived to the destination
    // onProgress -> The user has moved along the route
    // onUserOutsideRoute -> The user went off-route
    private NavigationListener navigationListener = new NavigationListener() {
        @Override
        public void onDestinationReached() {
            Log.i(TAG, "Destination Reached");
            isNavigating = false;
            SitumSdk.navigationManager().removeUpdates();
        }

        @Override
        public void onProgress(NavigationProgress navigationProgress) {
            Log.i(TAG, "Route advances");


            Log.i(TAG, "Current indication " + navigationProgress.getCurrentIndication().toText(myContext));
            Log.i(TAG, "Next indication " + navigationProgress.getNextIndication().toText(myContext));
            Log.i(TAG, "");
            Log.i(TAG, " Distance to goal: " + navigationProgress.getDistanceToGoal());
            Log.i(TAG, " Time to goal: " + navigationProgress.getTimeToGoal());
            Log.i(TAG, " Closest location in route: " + navigationProgress.getClosestLocationInRoute());
            Log.i(TAG, " Distance to closest location in route: " + navigationProgress.getDistanceToClosestPointInRoute());
            Log.i(TAG, "");
            Log.i(TAG, " Remaining segments: ");
            for (RouteSegment segment: navigationProgress.getSegments()){
                Log.i(TAG, "   Floor Id: " + segment.getFloorIdentifier());

                for (Point point: segment.getPoints()){
                    Log.i(TAG, "    Point: BuildingId "+ point.getFloorIdentifier() + " FloorId " + point.getFloorIdentifier() + " Latitude "+ point.getCoordinate().getLatitude() + " Longitude " + point.getCoordinate().getLongitude());
                }

                Log.i(TAG, "    ----");
            }

            Log.i(TAG, "--------");
        }

        @Override
        public void onUserOutsideRoute() {
            Log.i(TAG, "User outside of route");
            isNavigating=false;
            SitumSdk.navigationManager().removeUpdates();
        }
    };

Navigation progress updates as the route advances #

If you execute the previous example, you’ll notice a log like the following one:

I/MainActivity: --------
I/MainActivity: Updating navigation with: location = [Location{provider='SITUM_PROVIDER', deviceId=986700355878, timestamp=1665768227008, position=Point{buildingIdentifier='10194', floorIdentifier='29685', cartesianCoordinate=CartesianCoordinate{x=370.50, y=124.50}, coordinate=Coordinate{latitude=43.345468, longitude=-8.428541}, isOutdoor=false}, quality=HIGH, accuracy=3.0901935, cartesianBearing=Angle{radians=4.05, degrees=231.81}, bearing=Angle{radians=1.77, degrees=101.15}, pitch=Angle{radians=0.00, degrees=0.00}, roll=Angle{radians=0.00, degrees=0.00}, rotationMatrix=[0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0], hasRotationMatrix=false, bearingQuality=HIGH, customFields={}}]
I/MainActivity: Route advances
I/MainActivity: Current indication Turn right and go ahead for 25 metres
I/MainActivity: Next indication Turn left and go ahead for 215 metres
I/MainActivity: 
I/MainActivity:  Distance to goal: 300.7221581048454
I/MainActivity:  Time to goal: 300.7221581048454
I/MainActivity:  Closest location in route: Location{provider='SITUM_PROVIDER', deviceId=986700355878, timestamp=1665768227008, position=Point{buildingIdentifier='10194', floorIdentifier='29685', cartesianCoordinate=CartesianCoordinate{x=370.50, y=124.50}, coordinate=Coordinate{latitude=43.345468, longitude=-8.428541}, isOutdoor=false}, quality=HIGH, accuracy=3.0901935, cartesianBearing=Angle{radians=4.05, degrees=231.81}, bearing=Angle{radians=1.77, degrees=101.15}, pitch=Angle{radians=0.00, degrees=0.00}, roll=Angle{radians=0.00, degrees=0.00}, rotationMatrix=[0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0], hasRotationMatrix=false, bearingQuality=HIGH, customFields={}}
I/MainActivity:  Distance to closest location in route: 2.3647458431955726E-5
I/MainActivity: 
I/MainActivity:  Remaining segments: 
I/MainActivity:    Floor Id: 29685
I/MainActivity:     Point: BuildingId 29685 FloorId 29685 Latitude 43.34546807214645 Longitude -8.428541281424177
I/MainActivity:     Point: BuildingId 29685 FloorId 29685 Latitude 43.345312391294044 Longitude -8.42863516325766
I/MainActivity:     Point: BuildingId 29685 FloorId 29685 Latitude 43.34526171673805 Longitude -8.42865416981065
I/MainActivity:     Point: BuildingId 29685 FloorId 29685 Latitude 43.34474562906888 Longitude -8.428197910166963
I/MainActivity:     Point: BuildingId 29685 FloorId 29685 Latitude 43.34458693654681 Longitude -8.427917001970222
I/MainActivity:     Point: BuildingId 29685 FloorId 29685 Latitude 43.34438193534595 Longitude -8.427840175224832
I/MainActivity:     Point: BuildingId 29685 FloorId 29685 Latitude 43.34414696413134 Longitude -8.4274791606173
I/MainActivity:     Point: BuildingId 29685 FloorId 29685 Latitude 43.34373254393655 Longitude -8.427121661657521
I/MainActivity:     Point: BuildingId 29685 FloorId 29685 Latitude 43.34366724503788 Longitude -8.427207329649173
I/MainActivity:     Point: BuildingId 29685 FloorId 29685 Latitude 43.343528060658265 Longitude -8.42766592051245
I/MainActivity:     Point: BuildingId 29685 FloorId 29685 Latitude 43.3434333899059 Longitude -8.42761197887612
I/MainActivity:     ----
I/MainActivity: --------

In the previous log, we’re printing out some of the values of the NavigationProgress object (Android, iOS, Cordova, ReactNative), such as:

Implementing more complex navigation scenarios #

Situm SDK provides the building blocks so you can implement a navigation application, but how simple or complex this application gets is up to you. Here, we will summarize some common scenarios and how to solve them.

Allowing the user to select the destination point #

The previous snippet was a pet example, but obviously in a real application you should allow users to select their destination. This can be done in several ways, the most common being:

  • Implementing a POI search bar (e.g. like this one).
  • Showing the floorplans of the venue and the POIs, and allowing the user to click on them (e.g. like shown here).

Usually, you’ll want to implement navigation control buttons to start/stop the route & navigation, although this is not always necessary.

You may want to perform positioning and navigation in more than one building, for which most likely you’ll use Global Mode. You may change the order of operations in the previous example a bit to do so.

First, you should configure the LocationRequest appropriately for Global Mode. When the user is in a certain building, your application should request that building information. Probably, your app should show that building info too (e.g. showing the floorplans, the POIs… like we do in our WYF module).

At that point, you may allow the user to select a certain destination point using some of the techniques explained in the previous section.

User is out of the venue #

When the user is out of the venue your application will:

In any case, if this happens you should not start any navigation. This is because:

  • In Building Mode, you simply don’t know where the user is.
  • In Global Mode you know that the user is outdoors, but Situm does not support outdoor navigation.

If you try to compute a route, you’ll receive an error like the following one:

2022-10-08 17:47:02.732 15491-15491/com.example.helloworld E/MainActivity: ErrorError{domain=ROUTE, code=3031, description='request.getFrom() and request.getTo() are in different buildings. Routing between different buildings is not supported yet', properties=Bundle[{}]}

If the user is already navigating you should probably stop the navigation and inform the user (although this is up to you, depends on the kind of user experience you want to implement).

Adjusting the location to the route #

If you’re displaying the user location and the route on top of a floorplan, you’ll notice that the user location may not follow the route 100% (it may be off by a few meters). While this is OK, adjusting the location to be exactly on top of the route yields a better user experience.

To do this, you may use NavigationProgress closestLocationInRoute helper (Android, iOS, Cordova, ReactNative), which provides the route closest position to the user current’s location (preserving the orientation). For example, in Android:

private NavigationListener navigationListener = new NavigationListener() {
      ...

        @Override
        public void onProgress(NavigationProgress navigationProgress) {

            // You may display this location instead of the real user location
            // to achieve a smoother navigation experience           
            Log.i(TAG, " Closest location in route: " + navigationProgress.getClosestLocationInRoute());

            // Knowing the distance from the real location to this point may be handy.
            Log.i(TAG, " Distance to closest location in route: " + navigationProgress.getDistanceToClosestPointInRoute());
            ...

        }

       ...
    };

Your application may display this location instead of the real one to account for a smoother navigation experience. Our application Situm Mapping Tool allows you to test this behavour. This is also explained in the “Tips for building your wayfinding UX“.

Adjusting the location to the route

Redrawing the route in real-time while the user moves forward (or backwards) #

Provided that your application is displaying the route that the user is following, you may want to re-draw it as the user moves. This way, the route display will not be static (showing the whole route), but dynamic, showing only the part of the route that is left.

The good news is that you don’t have to re-compute the route each time, or keep track of the route progress. Every time the route advances, Situm SDK provides an easy way to access just the remaining segments. This way, you can re-draw the route from the adjusted location until the end, easily excluding the segments that have been already left behind. This is also explained in the “Tips for building your wayfinding UX“.

private NavigationListener navigationListener = new NavigationListener() {
        ...

        @Override
        public void onProgress(NavigationProgress navigationProgress) {
            
            
            Log.i(TAG, " Remaining segments: ");
            for (RouteSegment segment: navigationProgress.getSegments()){
                Log.i(TAG, "   Floor Id: " + segment.getFloorIdentifier());

                for (Point point: segment.getPoints()){
                    Log.i(TAG, "    Point: BuildingId "+ point.getFloorIdentifier() + " FloorId " + point.getFloorIdentifier() + " Latitude "+ point.getCoordinate().getLatitude() + " Longitude " + point.getCoordinate().getLongitude());
                }

                Log.i(TAG, "----");
            }
        }

        ...
    };

Re-evaluating indications #

Situm re-computes the turn-by-turn indications with each new location & orientation provided. This process works as explained in the following figure. Initially, Situm computes the route and a set of indications (initial set of indications). This set is not static: on the contrary, it is continuously reevaluated taken into account where the user is and the orientation that she is facing. Every time, Situm recomputes not only the current indication but the whole set. This means that even if the user doesn’t follow the indications correctly (e.g. takes a left turn instead of a right turn, like in the figure), Situm will always provide the right set of indications. This will allow you to show new indications dynamically as the user moves around the building.

Computing the Estimated Time of Arrival (ETA) #

A common feature in wayfinding apps is to provide the remaining distance to the end of the route and the estimated time of arrival. For this purpose, you may use the distanceToGoal (Android, iOS, Cordova, React Native) and timeToGoal (Android, iOS, Cordova, React Native) methods:

 private NavigationListener navigationListener = new NavigationListener() {
        ...

        @Override
        public void onProgress(NavigationProgress navigationProgress) {
            
            Log.i(TAG, " Distance to goal: " + navigationProgress.getDistanceToGoal());

            //Note that the "time to goal" assumes that the user walks at a speed of 1 m/s.
            Log.i(TAG, " Time to goal: " + navigationProgress.getTimeToGoal());
            
        }

        ...
    };

The following image shows a sample interface where we show the E.T.A to the user.

The routeStep helper (Android, iOS, Cordova, React Native), which provides the current route step, might also be handy when dealing with this functionality.

Stopping the navigation #

At any point, you may want to stop the navigation. You may do this using the NavigationManager’s removeUpdates method (Android, iOS, Cordova, ReactNative).

...

SitumSdk.navigationManager().removeUpdates();

Custom routes #

The navigation configuration also allows to defining custom routes. Custom routes utilize the paths outlined in Situm Dashboard (link) but factor in modifiers known as tags. These tags influence which segments of the route are included or excluded, allowing for tailored navigation beyond simply calculating the shortest or accessible path. For example, you might tag one link as “private” and then ask Situm to only provide the paths that are “private” or to avoid the “private” paths.

If you want to use these custom routes, you’ll need to:

  • Define the route tags in Situm Dashboard as explained here (link)
  • Configure Situm SDK to use these custom routes link

Subscribe to our newsletter

BASIC INFORMATION ON DATA PROTECTION

Data controller: SITUM TECHNOLOGIES, S.L.
Contact: Data controller: situm@situm.es
Responsible for protection: dpo@situm.es
Purpose and legal basis: To manage the sending of SITUM newsletters only with consent.
Legitimation: Express consent of the interested party.
Recipients: The data will not be passed on to third parties with the exception of legal obligations.
Retention period: As long as the interested party remains subscribed to the newsletter (a link to unsubscribe will be available in each newsletter sent by Situm).
Rights: The interested party may at any time revoke their consent, as well as exercise their rights of opposition, access, conservation, rectification, limitation, deletion of data and not be subject to a decision based only on automated data processing, by writing to SITUM at the addresses indicated.
Additional Information: You can consult additional and detailed information on Data Protection in our privacy policy.

Please, download your copy here

Thank you for downloading our whitepaper. Please do not hesitate to contact us if you would like to know more about how our solutions can help your business. Download whitepaper


Close window