Skip to content

Navigation

This section explains the implementation of real-time turn-by-turn navigational instructions, both as visual and voice instructions.

Position Manager

There are several ways how to work with your Position in Sygic Maps SDK. You can leave everything to the SDK, or you can use the CustomPositionUpdater and pass it your own values. Let's get straight into it. To to get and see your position, you simply need to initialize the PositionManager and start the location updating:

val mPositionManager = PositionManagerProvider.getInstance().get()
mPositionManager.startPositionUpdating()

Attention

Always get the managers after initializing the engine.
The SDK chooses between the Google Play services location APIs ( uses the FusedLocationProviderClient and PRIORITY_HIGH_ACCURACY) or the Android framework location APIs depending on whether you have Google Play Services enabled.

Info

You need to request the location permission from the user, else the location won't work.

Starting from Android 10 you also need to put

<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION">
// or
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION">

in your Android manifest file.

If you would like to show the "Allow all the time" option when requesting a permission, you will also need the ACCESS_BACKGROUND_LOCATION permission. You can read more about the permissions on Android 10 and higher on the official Android Developer site.

Custom position updater

Otherwise, you can use the CustomPositionUpdater to update your position in Sygic Maps SDK. You can pass any coordinates that you read from a file beforehand, get from a server or get from the Android Location module etc. Let's see how to integrate the Android Location module updates:

private val mCustomPositionUpdater = CustomPositionUpdater()
val mPositionManager = PositionManagerProvider.getInstance().get() // you always need to get the instance
val locationListener = object : LocationListener {
  override fun onLocationChanged(location: Location?) {
    location?.let {
      mCustomPositionUpdater.updatePosition(GeoPosition(GeoCoordinates(location.latitude, location.longitude), location.speed.toDouble(), location.bearing, location.time/1000))
    }
  }
  .... // other callbacks
}
val locationManager = this.getSystemService(Context.LOCATION_SERVICE) as LocationManager
locationManager.requestLocationUpdates(LocationManager.GPS_PROVIDER, 1000, 0.1f, locationListener)

The Sygic Maps SDK for Android supports navigation on pedestrian and car routes. Using this feature, your app can check the current device position against a calculated route and provide just-in-time navigational instructions, both as visual and voice instructions. Turn-by-Turn Navigation Mode, which takes the calculated route and matches the current position against the route is supported for walking and driving.

While navigating, application can register a listener for warning notifications and display all of these notifications. All the types of notifications are listed in NavigationManager class. The following table shows the name of the available listeners and the information provided by them.

Listener name Function
OnBetterRouteListener Whether a faster route has been found
OnDirectionListener Whether the direction has changed
OnSpeedLimitListener Whether the user has exceeded the speed limit
OnSignpostListener Whether the signpost has updated
OnLaneListener Whether the lane information has changed
OnPlaceListener Whether the nearby place info has changed
OnIncidentListener Whether and incident is ahead
OnRailwayCrossingListener Whether a railway crossing is ahead
OnHighwayExitListener Whether a highway exit is ahead
OnSharpCurveListener Whether a sharp curve is ahead
OnWaypointPassListener Whether the vehicle passed by a waypoint
OnTrafficChangedListener Whether the traffic data has updated
OnRouteChangedListener Whether the route has changed
OnRecomputeProgressListener Whether the route recompute progress has changed (100 is the max value)
OnBatteryCapacityListener Whether the battery capacity has changed (new in version 17)
WaypointOutOfRangeListener Whether any waypoint becomes out of reach (new in version 17.x)

Registering a listener has the following pattern:

// create listener
mOnSpeedLimitListener = new NavigationManager.OnSpeedLimitListener() {
    @Override
    public void onSpeedLimitInfoChanged(@NonNull SpeedLimitInfo speedLimitInfo) {
        // info about changed value
        Log.d("TAG", "SpeedLimit: " + speedLimitInfo.getSpeedLimit());
        Log.d("TAG", "NextSpeedLimit: " + speedLimitInfo.getNextSpeedLimit());
        // ...
    }
};

// register the listener
NavigationManagerProvider.getInstance(new CoreInitCallback<NavigationManager>() {
    @Override
    public void onInstance(@NonNull NavigationManager navigationManager) {
        navigationManager.addOnSpeedLimitListener(mOnSpeedLimitListener);
    }
    @Override
    public void onError(@NonNull CoreInitException e) {
    }
});

Don't forget to unregister the listener when you don't need it.

navigationManager.removeOnSpeedLimitListener(mOnSpeedLimitListener);

Map Matching During Navigation

Map Matching is automatically enabled in both navigation mode and tracking mode. In simulation mode, the map-matched position is simulated along the route with a user-defined speed.

Handling route changes when you steer away from the route

When you leave the route that you have chosen for navigation, the SDK handles the recompute internally and sets the new route for navigation as well. All you need to do is to remove the old MapRoute and add a new one, using the route that you get in the onRouteChanged listener callback.

Voice Instructions

Voice instructions are available in the Sygic Maps SDK as voice packages. Voice packages are available in two forms: pre-packaged or downloadable through the voice catalog. You can set a voice package to be used for navigational instructions. More about audio options can be found here

The Sygic Maps SDK supports two types of voice packages: text-to-speech or pre-recorded. Pre-recorded voice skins provide basic maneuver instructions, such as "turn right in 100 meters" instruction, while text-to-speech voices also support spoken street names, such as "turn right in 100 meters onto Wall Street" instruction.

At first, you need to get the VoiceManager and VoiceDownload and register them:

VoiceDownload mVoiceDownload;
VoiceManager mVoiceManager;

VoiceDownloadProvider.getInstance(new CoreInitCallback<VoiceDownload>() {
  @Override
  public void onInstance(@NonNull VoiceDownload voiceDownload) {
    mVoiceDownload = voiceDownload;
  }

  @Override
  public void onError(@NonNull CoreInitException e) {
  }
});
VoiceManagerProvider.getInstance(new CoreInitCallback<VoiceManager>() {
  @Override
  public void onInstance(@NonNull VoiceManager voiceManager) {
    mVoiceManager = voiceManager;
  }

  @Override
  public void onError(@NonNull CoreInitException e) {
  }
});

The following code shows how to get the list of installed voices and how to set the voice:

mVoiceManager.getInstalledVoices(new VoiceManager.InstalledVoicesCallback() {
    @Override
    public void onInstalledVoiceList(final VoiceEntry[] voices, @NonNull final OperationStatus status) {
        mVoiceManager.setVoice(voices[0]);
    }
});

To get all the available voices, you call VoiceDownload.getAvailableVoices():

mVoiceDownload.getAvailableVoiceList(new VoiceDownload.AvailableVoicesCallback() {
    @Override
    public void onAvailableVoiceList(final VoiceEntry[] voiceEntries, @NonNull final OperationStatus operationStatus) {
        for (final VoiceEntry entry : voiceEntries) {
            String name = entry.getName();
            String language = entry.getLanguage();
            boolean tts = entry.isTts();
            switch (entry.getStatus()) {
                case VoiceEntry.VoicePackageStatus.NotInstalled:
                    //...
                    break;
                case VoiceEntry.VoicePackageStatus.Installed:
                    //...
                    break;
                default:
                    //...
            }
        }
    }
});

Then you can download voices.

 mVoiceDownload.addVoiceInstallationListener(new VoiceInstallListener() {
     @Override
     public void onVoiceInstallProgress(VoiceEntry voice, long bytesDownloaded, long totalBytes) {

     }

     @Override
     public void onVoiceInstallFinished(final VoiceEntry voice, @NonNull final OperationStatus status) {

     }

     @Override
     public void onVoiceUninstallFinished(final VoiceEntry voice, @NonNull final OperationStatus status) {

     }
});

 mVoiceDownload.installVoice(voice); // entry is an object returned in onAvailableVoiceList

You can register an AudioInstructionListener in which you can either tell our SDK to play the instruction (returning false from the method, like below) or play any information from the direction info however you want (in that case, you need to return true in the callback).

mNavigationManager.setAudioInstructionListener(new NavigationManager.AudioInstructionListener() {
    @Override
    public boolean onAudioInstruction(DirectionInfo directionInfo) {
        return false;
    }
});

This is a list of the potential TTS languages that are supported. Actual audio playback depends on the supported languages in the user's installed version of Android.

  • English (US)
  • English (UK)
  • French (France)
  • French (Canada)
  • German
  • Spanish (Spain)
  • Spanish (Mexico)
  • Indonesian
  • Italian
  • Norwegian
  • Portuguese (Portugal)
  • Portuguese (Brazil)
  • Russian
  • Swedish
  • Finnish
  • Danish
  • Korean
  • Chinese (Taiwanese Mandarin)
  • Turkish
  • Czech
  • Polish

Traffic

Traffic information show traffic incidents on the map. Traffic visualization requires a network data connection to download real time traffic information. However, traffic information may continue to be displayed thereafter without a connection until the traffic events expire or the visibility is toggled.

You can turn traffic information on and off like this:

mTrafficManager = TrafficManagerProvider.getInstance().get()
mTrafficManager.enableTrafficService()
mTrafficManager.disableTrafficService()

There are different types of traffic information and how it's displayed on map. The most common ones are:

  • closure (always grey),
  • traffic jam (red, orange, green depending on how big the delay is),
  • roadworks (green, yellow, red - depending on the delay),
  • some local events like icy roads or strong wind.

Traffic information

Signposts

Signs represent textual and graphic information which are along the route. The information is always represented as text or pictogram. Signpost information may be used for route guidance and map display. A navigation system may prefer using the signpost text rather than the street/ramp name as the latter may not always match what is on the sign in reality and may confuse a user. The signpost feature supports the user navigating through complex situations and provides a conformation for a maneuver by presenting the same direction information as shown on the street signs in reality.

Based on the map attributes, the Sygic Maps SDK aggregates the following information into a Signpost object:

  • Route number
  • Exit number
  • Exit name
  • Pictogram
  • Place name
  • Background color
  • Text color

Following example shows, how to get these information from the data structure, which the Android SDK offers.

import android.os.Bundle;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.support.v4.app.Fragment;

import com.sygic.sdk.navigation.NavigationManager;
import com.sygic.sdk.navigation.routeeventnotifications.SignpostInfo;

import java.util.ArrayList;
import java.util.List;

public class SignpostFragment extends Fragment implements NavigationManager.OnSignpostListener {

    private NavigationManager mManager;

    @Override
    public void onCreate(@Nullable final Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        NavigationManagerProvider.getInstance(new CoreInitCallback<NavigationManager>() {
            @Override
            public void onInstance(@NonNull NavigationManager navigationManager) {
                mManager = navigationManager;
            }
            @Override
            public void onError(@NonNull CoreInitException e) {
            }
        });
        mManager.addOnSignpostListener(this);
    }
    @Override
    public void onDestroy() {
        super.onDestroy();
        mManager.removeOnSignpostListener(this);
    }

    // ....

    @Override
    public void onSignpostChanged(@NonNull List<SignpostInfo> list) {
        if (list.isEmpty()) {
            return;
        }

        final SignpostInfo signpostInfo = list.get(0);
        final int textColor = signpostInfo.getTextColor();
        final int backgroundColor = signpostInfo.getBackgroundColor();
        final int distanceToSignpost = signpostInfo.getDistance();

        final List<String> titles = new ArrayList<>();
        final List<String> roadNumbers = new ArrayList<>();
        final List<String> exits = new ArrayList<>();
        final List<String> exitNumbers = new ArrayList<>();

        for (SignpostInfo.SignElement signElement : signpostInfo.getSignElements()) {
            switch (signElement.getElementType()) {
                case SignpostInfo.SignElement.SignElementType.PlaceName:
                case SignpostInfo.SignElement.SignElementType.StreetName:
                case SignpostInfo.SignElement.SignElementType.OtherDestination:
                    titles.add(signElement.getText());
                    break;
                case SignpostInfo.SignElement.SignElementType.RouteNumber:
                    roadNumbers.add(signElement.getText());
                    break;
                case SignpostInfo.SignElement.SignElementType.ExitName:
                    exits.add(signElement.getText());
                    break;
                case SignpostInfo.SignElement.SignElementType.ExitNumber:
                    exitNumbers.add(signElement.getText());
                    break;
            }
        }

        // do something with the collected data
    }
}

Directions

To provide more detailed information in signpost, you can display the direction which the user should follow. The Directions feature allows developers to define and display routes between a start and a destination point within their application. NavigationManager.OnDirectionListener can return the following information:

  • Direction distance
  • Next direction distance
  • Maneuver type - see the all available ManeuverTypes
  • Current and next road names
  • Current and next road numbers
  • Whether you are in a tunnel and the remaining distance to its end

The following example shows how to get these information from the data structure, which the Android SDK offers.

import android.os.Bundle;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.support.v4.app.Fragment;
import android.util.Log;

import com.sygic.sdk.navigation.NavigationManager;
import com.sygic.sdk.navigation.routeeventnotifications.DirectionInfo;
import com.sygic.sdk.route.RouteManeuver;

public class DirectionFragment extends Fragment implements NavigationManager.OnDirectionListener {

    private NavigationManager mManager;

    @Override
    public void onCreate(@Nullable final Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        NavigationManagerProvider.getInstance(new CoreInitCallback<NavigationManager>() {
            @Override
            public void onInstance(@NonNull NavigationManager navigationManager) {
                mManager = navigationManager;
            }
            @Override
            public void onError(@NonNull CoreInitException e) {
            }
        });
        mManager.addOnDirectionListener(this);
    }
    @Override
    public void onDestroy() {
        super.onDestroy();
        mManager.removeOnDirectionListener(this);
    }

    // ....

    @Override
    public void onDirectionInfoChanged(@NonNull final DirectionInfo directionInfo) {
        final int distance = directionInfo.getDistance();
        final int nextDistance = directionInfo.getNextDistance();

        final RouteManeuver primaryManeuver = directionInfo.getPrimary();
        if (primaryManeuver != null) {
            final int type = primaryManeuver.getType();
            final String roadName = primaryManeuver.getRoadName();
            final String nextRoadName = primaryManeuver.getNextRoadName();
            final List<String> nextRoadNumbers = primaryManeuver.getNextRoadNumbers();
            Log.d("directions", "direction of type " + type + " on the road " + roadName + ". Next road is " + nextRoadName);
        }

    }
}

With these data now you just have to draw them into your view and create your own signposts. For example like this:

Signpost

Junction Info

Junction info is one of our advanced safety features. When you are approaching a complex highway intersection, a SingpostJunctionInfo object will be set in a OnSignpostListener. It contains info about lanes both continuing on highway and on the exit. You can then display this information, e.g. as full screen diagram.

mNaviSignListener = new NavigationManager.OnSignpostListener() {
    @Override
    public void onSignpostChanged(@NonNull final List<SignpostInfo> list) {
        for (final SignpostInfo signpostInfo : list) {
            SignpostJunctionInfo junctionInfo = signpostInfo.getJunctionInfo();

            if (junctionInfo.getAreaType() != SignpostJunctionInfo.AreaType.Unknown) {
                @SignpostJunctionInfo.TurnDirection int turnDirection = junctionInfo.getTurnDirection();
                @SignpostJunctionInfo.TurnType int turnType = junctionInfo.getTurnType();
                int fromLanes = junctionInfo.getFromLanesCount();
                int leftLanes = junctionInfo.getToLeftLanesCount();
                int rightLanes = junctionInfo.getToRightLanesCount();

                // display junction info

                return;
            }
        }

        // no junction info
    }
};

When you have SignpostJunctionInfo with getFromLanesCount() == 5, getToLeftLanesCount() == 4, getToRightLanesCount() == 2, it can be displayed as shown below.

Junction

Speed Limit

Speed limits indicate the legal speed for a vehicle. You can get the value either in mph or in km/h. You can register OnSpeedLimitListener to receive events about speed limit change. The class containing information about speed limit is SpeedLimitInfo.

mSpeedLimitListener = new NavigationManager.OnSpeedLimitListener() {
    @Override
    public void onSpeedLimitInfoChanged(@NonNull final SpeedLimitInfo speedLimitInfo) {
        int speedLimit = speedLimitInfo.getSpeedLimit(SpeedLimitInfo.SpeedUnits.Kilometers);
        int currentSpeed = speedLimitInfo.getCurrentSpeed(SpeedLimitInfo.SpeedUnits.Kilometers);

        if (speedLimit < currentSpeed) {
            // vehicle is speeding
        }

        int nextSpeedLimit = speedLimitInfo.getNextSpeedLimit(SpeedLimitInfo.SpeedUnits.Kilometers);
        int distance = speedLimitInfo.getNextSpeedLimitDistance();

        if (speedLimit != nextSpeedLimit) {
            // eg. show warning about limit change
        }

        @SpeedLimitInfo.SpeedUnits int countrySpeedUnits = speedLimitInfo.getCountrySpeedUnits();
        int naturalLimit = speedLimitInfo.getSpeedLimit(countrySpeedUnits);

        // natural limit is in country's natural units, so rounding and decimals can be avoided
    }
};

Route Simulation

You can quickly start a route simulation by requesting an instance of RouteDemonstrateSimulator and passing it the computed route:

Do not forget to call .stop() upon cancelling the route or if you no longer need the simulation to prevent unwanted behavior.

Also please note that you need to call the navigationManager's setRouteForNavigation before starting the simulator.

RouteDemonstrateSimulatorProvider.getInstance(mRoute, new CoreInitCallback<RouteDemonstrateSimulator>() {
    @Override
    public void onInstance(@NonNull RouteDemonstrateSimulator routeDemonstrateSimulator) {
        routeDemonstrateSimulator.start();
    }

    @Override
    public void onError(@NonNull CoreInitException e) {
        ...
    }
});

Scout Compute

What is the scout compute and how does it work?

Let's imagine the user is navigating home from work and new traffic jams are reported along his route. The user would like to know this and be offered a new, faster route. This is what the scout compute does - it's how we call it, but you will actually use the OnBetterRouteListener to achieve the results. The scout compute runs each 10 minutes in the offline compute and each 5 minutes if you chose the online compute. The system will take several variables into account before offering the better route - for example, a new route won't be offered if the time that the user would save is less than 1 minute etc.

Here's how to work with the OnBetterRouteListener. In this case, the it is an object of BetterRouteInfo.

 mNavigationManager.addOnBetterRouteListener {
   println("Great! A faster route has been found")
   println("Saved time: " + it.timeDiff)
   println("Saved distance: " + it.lengthDiff + "m")
   println("Point of detour: " + it.splitPoint.toString()) // GeoCoordinates
   // you can show it to the user on the map and let him decide
   // if the user chooses the new one, you need to set it for navigation
   mNavigationManager.setRouteForNavigation(it.alternativeRoute)
   // also don't forget to remove the old route from the mapView and add the new one!
 }

Adjust the scout compute options

Since the version 19.3.0, several adjustments can be made to the way the scout compute is calculated. You can adjust these to your own liking in the JSON Configuration via the Navigation -> ScoutSettings. This comprises the online and offline update intervals, the minimal saved time and the coefficient of the saved time.

To explain all of those, let's start with the intervals. As we have seen above, the default values for offline and online scout triggers are 10 and 5 minutes, respectively. To adjust the interval of the offline scout compute, change the value of offline_update_interval (it is in seconds).

To adjust the interval of the online scout compute, change the value of online_update_interval.

Note

Please note that the minimal value of the online interval is 5 minutes (300 seconds). If you enter a smaller value, it will automatically default to 300 seconds.

To adjust the value of the time difference that's necessary to exist between the two routes for the scout compute to notify you, change the value of minimal_saved_time. The value should be in seconds.

The saved_time_coefficient is yet another value which determines whether the new route will be offered or not. For the scout route to be offered, its time has to be lower than the actual remaining time, the time difference between the two routes has to be bigger than the minimal_saved_time value and the time difference also has to be higher than the minimum of the two:

  • saved_time_coefficient times the actual remaining time
  • 10 minutes

Info

That means if your actual remaining time is 5 minutes, the time of the new route is 2 minutes, your minimal saved time is set to 60 seconds and the saved time coefficient is set to 0.05, the new route will be offered, because 120s < 300s, the saved time (180s) is higher than 60s and the saved time (180s) is higher than 0.05*300 = 15s.

Audio

Here you can find more information about the Audio settings and features.

Let's play an audio output via the SDK:

val mAudioManager = AudioManagerProvider.getInstance().get()
mAudioManager.playOutput(AudioTTSOutput("Your text")) //plays text using the TTS
mAudioManager.playOutput(AudioFileOutput(listOf("yourfile.wav"))) // plays your audio file (wav or ogg)

You can also choose between various modes of Audio output:

  • Default - plays via your phone's speakers or the bluetooth device, if you're connected to one,
  • DefaultSpeaker - plays the output on your device even if you're connected to an external speaker,
  • BluetoothHFP - uses the hands-free profile to "call" the device that you're connected to.

Warning

If you choose the BluetoothHFP option, you should make sure that you let the users choose their own delay as different devices behave differently and the audio may be cut out.

mAudioManager.audioRoute = AudioManager.AudioRoute.BluetoothHFP
mAudioManager.setHfpDelay(1000) // in miliseconds

Additionally, several of the warning sound settings can be changed using the AudioSettings. Let's see some of them:

val audioSettings = AudioSettingsProvider.getInstance().get()
audioSettings.readRoadNames = false // to turn off reading of the road names
audioSettings.unitSettings = AudioSettings.DistanceUnits.Kilometers // to set different units
audioSettings.ttsSpeedCamWarnText = "There's a speed camera on your route!" // to override the warning text

Incidents

Our Speed Camera database consists of more than 110 000 fixed speed cameras, red light cameras and average speed checks all over the world. On top of that, the community reports tens of thousands of additional mobile speed cameras and police checks every day. Incidents also include school zones, crashes and more.

How to handle incidents?

You can use two ways to obtain information about incidents on your route:

  • the OnIncidentListener analyzer that can be found in the NavigationManager
  • the exploreIncidentsOnRoute() that can be used via the RouteExplorer

You can retrieve basic information about incidents ahead of you by using the OnIncidentListener:

mNavigationManager.addOnIncidentListener {
    it.distance // distance to the incident
    it.incidentLink // the incident link
    it.recommendedSpeed // the recommended speed
}

The recommended speed does not change for a fixed speed camera. Therefore, while you are approaching a speed camera, the speed stays at 60km/h for example. However, section radars exist, too. These are the RadarStaticAverageSpeed, RadarStaticAverageSpeedMiddle and RadarStaticAverageSpeedEnd ones. If you approach one of the latter two, the recommendedSpeed changes according to the speed of the vehicle - it is calculated internally using the speed and distance.

The incident link contains information about the location, provider, or type.

Simple Lane Assistant

It's possible to retrieve simple lane information on some roads. This is necessary if you would like to show the arrows on your route. You get the information via the Navigation Manager's OnLaneListener.

Arrows

At first you would need to create functions that would convert the instructions into images. There's two of them, because you may get two Arrows in a single lane - that's where we use the stackable resource.

@DrawableRes
fun getDirectionDrawableResStackable(@LaneInfo.Lane.Direction direction: Int): Int {
    return when (direction) {
        LaneInfo.Lane.Direction.Straight -> R.drawable.laneassist_stackable_straight
        LaneInfo.Lane.Direction.Left -> R.drawable.laneassist_stackable_left
        LaneInfo.Lane.Direction.HalfLeft -> R.drawable.laneassist_stackable_halfleft
        LaneInfo.Lane.Direction.SharpLeft -> R.drawable.laneassist_stackable_sharpleft
        LaneInfo.Lane.Direction.Right -> R.drawable.laneassist_stackable_right
        LaneInfo.Lane.Direction.HalfRight -> R.drawable.laneassist_stackable_halfright
        LaneInfo.Lane.Direction.SharpRight -> R.drawable.laneassist_stackable_sharpright
        LaneInfo.Lane.Direction.UTurnLeft -> R.drawable.laneassist_stackable_uturnleft
        LaneInfo.Lane.Direction.UTurnRight -> R.drawable.laneassist_stackable_uturnright
        else -> 0
    }
}

@DrawableRes
fun getDirectionDrawableRes(@LaneInfo.Lane.Direction direction: Int): Int {
    return when (direction) {
        LaneInfo.Lane.Direction.Straight -> R.drawable.laneassist_straight
        LaneInfo.Lane.Direction.Left -> R.drawable.laneassist_left_45
        LaneInfo.Lane.Direction.HalfLeft -> R.drawable.laneassist_left_90
        LaneInfo.Lane.Direction.SharpLeft -> R.drawable.laneassist_left_135
        LaneInfo.Lane.Direction.Right -> R.drawable.laneassist_right_45
        LaneInfo.Lane.Direction.HalfRight -> R.drawable.laneassist_right_90
        LaneInfo.Lane.Direction.SharpRight -> R.drawable.laneassist_right_135
        LaneInfo.Lane.Direction.UTurnLeft -> R.drawable.laneassist_left_180
        LaneInfo.Lane.Direction.UTurnRight -> R.drawable.laneassist_right_180
        else -> 0
    }
}

Then let's create a data class Arrow that will represent one arrow. This way the work with an arrow will be easier, but you may create your own representation.

data class Arrow(val isSelected: Boolean, @DrawableRes val directionDrawableRes: Int) {}

And next we can work with the listener like this :

// here we get the navigation manager
mNavigationManager = NavigationManagerProvider.getInstance().get()
// register a listener
mNavigationManager.addOnLaneListener { laneInfo ->
    val simpleLanesInfo = laneInfo.simpleLanesInfo
    val arrows = mutableListOf<Arrow>()

    simpleLanesInfo?.let {
        val lanes = simpleLanesInfo.lanes
        lanes.forEach { lane ->
            // if there are more direction arrows in one lane
            if (lane.arrows.size > 1) {
                lane.arrows.forEach { arrow ->
                    // we will create an Arrow object and add it to the list
                    arrows.add(Arrow(arrow.isHighlighted, getDirectionDrawableResStackable(arrow.direction)))
                }
            } else {
                // if there's just one direction arrow in a lane
                lane.arrows.forEach {arrow ->
                    // we will create an Arrow object and add it to the list
                    arrows.add(Arrow(arrow.isHighlighted, getDirectionDrawableRes(arrow.direction)))
                }
            }
        }
    }
    // then you would return the arrows to where you will draw them
}