0


I'm having troubles trying to setup a listener for Milestone events with a custom Milestone. I followed the instructions on the documentation but the code inside the Milestone event does not seem to fire at all. My goal is to get a BannerInstruction and fire the default instruction 10 meters before the next step. The banner instruction test (ex. "turn left") will then be sent over BLE (I'm using RxAndroidBle library) to another device.

So far i succeded in sending banner instructions over bluetooth by triggering a willDisplay listener (which activates everytime there's a change in the instructions, but the problem with that is that i have no control over the time the instruction will be displayed.

I'm a beginner in Java so i'm not completely sure if what i did was right. Please also don't mind the badly written code, i'm still learning!

This is MainActivity.java:

    package com.bpnavi.backpacknavigator;

import android.bluetooth.BluetoothAdapter;
import android.content.Context;
import android.content.Intent;
import android.support.annotation.Nullable;
import android.support.v7.app.AppCompatActivity;
import android.os.Bundle;

import com.mapbox.api.directions.v5.DirectionsCriteria;
import com.mapbox.api.directions.v5.MapboxDirections;
import com.mapbox.api.directions.v5.models.BannerInstructions;
import com.mapbox.mapboxsdk.annotations.Marker;
import com.mapbox.mapboxsdk.annotations.MarkerOptions;
import com.mapbox.mapboxsdk.camera.CameraPosition;
import com.mapbox.mapboxsdk.geometry.LatLng;
import com.mapbox.mapboxsdk.Mapbox;
import com.mapbox.mapboxsdk.location.modes.RenderMode;
import com.mapbox.mapboxsdk.maps.MapboxMap;
import com.mapbox.mapboxsdk.maps.MapView;



// classes needed to add the location component
import com.mapbox.geojson.Point;
import com.mapbox.mapboxsdk.maps.MapboxMap;
import com.mapbox.mapboxsdk.maps.OnMapReadyCallback;
import android.location.Location;
import android.view.View;
import android.view.Window;
import android.widget.Button;
import android.widget.Toast;
import com.mapbox.mapboxsdk.geometry.LatLng;
import android.support.annotation.NonNull;
import com.mapbox.mapboxsdk.location.LocationComponent;
import com.mapbox.mapboxsdk.location.modes.CameraMode;
import com.mapbox.services.android.navigation.ui.v5.NavigationLauncherOptions;
import com.mapbox.android.core.permissions.PermissionsListener;
import com.mapbox.android.core.permissions.PermissionsManager;


// classes to calculate a route
import com.mapbox.services.android.navigation.ui.v5.NavigationView;
import com.mapbox.services.android.navigation.ui.v5.NavigationViewOptions;
import com.mapbox.services.android.navigation.ui.v5.OnNavigationReadyCallback;
import com.mapbox.services.android.navigation.ui.v5.listeners.BannerInstructionsListener;
import com.mapbox.services.android.navigation.ui.v5.route.NavigationMapRoute;
import com.mapbox.services.android.navigation.v5.instruction.Instruction;
import com.mapbox.services.android.navigation.v5.milestone.Milestone;
import com.mapbox.services.android.navigation.v5.milestone.MilestoneEventListener;
import com.mapbox.services.android.navigation.v5.milestone.RouteMilestone;
import com.mapbox.services.android.navigation.v5.milestone.StepMilestone;
import com.mapbox.services.android.navigation.v5.milestone.Trigger;
import com.mapbox.services.android.navigation.v5.milestone.TriggerProperty;
import com.mapbox.services.android.navigation.v5.navigation.NavigationEventListener;
import com.mapbox.services.android.navigation.v5.navigation.NavigationRoute;
import com.mapbox.api.directions.v5.models.DirectionsResponse;
import com.mapbox.api.directions.v5.models.DirectionsRoute;
import com.mapbox.services.android.navigation.v5.navigation.MapboxNavigation;
import com.mapbox.services.android.navigation.v5.navigation.MapboxNavigationOptions;
import com.mapbox.services.android.navigation.v5.routeprogress.ProgressChangeListener;
import com.mapbox.services.android.navigation.v5.routeprogress.RouteProgress;
import com.mapbox.services.android.navigation.v5.routeprogress.RouteProgressState;
import com.mapbox.services.android.navigation.v5.utils.RouteUtils;

import io.reactivex.Observable;
import io.reactivex.disposables.Disposable;
import retrofit2.Call;
import retrofit2.Callback;
import retrofit2.Response;
import timber.log.Timber;


// classes needed to launch navigation UI
import android.view.View;
import android.widget.Button;
import com.mapbox.services.android.navigation.ui.v5.NavigationLauncher;

import com.mapbox.services.android.navigation.v5.offroute.OffRouteListener;
import com.mapbox.services.android.navigation.v5.routeprogress.ProgressChangeListener;
import com.mapbox.services.android.navigation.v5.routeprogress.RouteProgress;
import com.polidea.rxandroidble2.RxBleClient;
import com.polidea.rxandroidble2.RxBleConnection;
import com.polidea.rxandroidble2.RxBleDevice;
import com.polidea.rxandroidble2.internal.RxBleLog;
import com.polidea.rxandroidble2.scan.ScanSettings;

import android.util.Log;



import java.util.List;
import java.util.UUID;


public class MainActivity extends AppCompatActivity implements PermissionsListener, MapboxMap.OnMapClickListener, OnNavigationReadyCallback, BannerInstructionsListener,
        MilestoneEventListener, OffRouteListener, ProgressChangeListener {
    private MapView mapView;
    private NavigationView navigationView;

    private MapboxMap mapboxMap;
    private PermissionsManager permissionsManager;
    private Location originLocation;

    // variables for adding a marker
    private Marker destinationMarker;
    private LatLng originCoord;
    private LatLng destinationCoord;

    // variables for calculating and drawing a route
    private Point originPosition;
    private Point destinationPosition;
    private DirectionsRoute currentRoute;
    private static final String TAG = "DirectionsActivity";
    private NavigationMapRoute navigationMapRoute;
    private MapboxDirections client;
    private Button startButton;

    public static final String BACKPACK_DEVICE_ADDRESS = "123";
    public static final String TEST_DEVICE_ADDRESS = "123";
    private static final UUID WRITE_CHARACTERISTIC = UUID.fromString("abc123");
    private Disposable disposable;
    private RxBleDevice device;
    private BannerInstructions BannerInstructionMilestone;
    private MapboxNavigation navigation;

    private static final int INSTR_MILESTONE = 1001;

    @Override
    protected void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);

        RxBleClient rxBleClient = RxBleClient.create(this);

        Disposable scanSubscription = rxBleClient.scanBleDevices(
                new ScanSettings.Builder()
                        // .setScanMode(ScanSettings.SCAN_MODE_LOW_LATENCY) // change if needed
                        // .setCallbackType(ScanSettings.CALLBACK_TYPE_ALL_MATCHES) // change if needed
                        .build()
                // add filters if needed
        )
                .subscribe(
                        scanResult -> {
                            // Process scan result here.
                        },
                        throwable -> {
                            // Handle an error here.
                        }
                );

        // When done, just dispose.
        scanSubscription.dispose();

        Disposable flowDisposable = rxBleClient.observeStateChanges()
                .switchMap(state -> { // switchMap makes sure that if the state will change the rxBleClient.scanBleDevices() will dispose and thus end the scan
                    switch (state) {

                        case READY:
                            // everything should work
                            return rxBleClient.scanBleDevices();
                        case BLUETOOTH_NOT_AVAILABLE:
                            // basically no functionality will work here
                        case LOCATION_PERMISSION_NOT_GRANTED:
                            // scanning and connecting will not work
                        case BLUETOOTH_NOT_ENABLED:
                            // scanning and connecting will not work
                        case LOCATION_SERVICES_NOT_ENABLED:
                            // scanning will not work
                        default:
                            return Observable.empty();
                    }
                })
                .subscribe(
                        rxBleScanResult -> {
                            // Process scan result here.
                        },
                        throwable -> {
                            // Handle an error here.
                        }
                );

                // When done, just dispose.
                flowDisposable.dispose();



        device = rxBleClient.getBleDevice(TEST_DEVICE_ADDRESS);

        Disposable disposable = device.establishConnection(true) // <-- autoConnect flag
                .subscribe(
                        rxBleConnection -> {
                            // All GATT operations are done through the rxBleConnection.
                        },
                        throwable -> {
                            // Handle an error here.
                        }
                );
//        disposable.dispose();


        Mapbox.getInstance(this, getString(R.string.access_token));
        setContentView(R.layout.activity_main);
        mapView = (MapView) findViewById(R.id.mapView);
        startButton = findViewById(R.id.startButton);

        startButton.setOnClickListener(v -> {
            setTheme(R.style.Theme_AppCompat_NoActionBar);
            setContentView(R.layout.activity_navigation);
            navigationView = findViewById(R.id.navigationView);
            navigationView.onCreate(savedInstanceState);
            navigationView.initialize(this);

//            boolean simulateRoute = true;
//            NavigationLauncherOptions options = NavigationLauncherOptions.builder()
//                    .directionsRoute(currentRoute)
//                    .shouldSimulateRoute(simulateRoute)
//                    .build();
//            // Call this method with Context from within an Activity
//            NavigationLauncher.startNavigation(MainActivity.this, options);
        });

        MapboxNavigationOptions navOptions = MapboxNavigationOptions.builder().isDebugLoggingEnabled(true).build();
        navigation = new MapboxNavigation(getApplicationContext(), getString(R.string.access_token), navOptions);
        navigation.addMilestoneEventListener(this);


        navigation.addMilestone(new StepMilestone.Builder()
                .setIdentifier(INSTR_MILESTONE)
                .setInstruction(this.myInstruction)
                .setTrigger(
                        Trigger.all(
                                Trigger.lt(TriggerProperty.STEP_DISTANCE_REMAINING_METERS, 10)))
                .build()
        );

        mapView.onCreate(savedInstanceState);
        mapView.getMapAsync(mapboxMap -> {
            MainActivity.this.mapboxMap = mapboxMap;
            enableLocationComponent();

            originCoord = new LatLng(originLocation.getLatitude(), originLocation.getLongitude());

            mapboxMap.addOnMapClickListener(this);

            //Set stuff here
            mapboxMap.addMarker(new MarkerOptions()
                    .position(new LatLng(45.506833, 9.163333))
                    .title("Politecnico di Milano")
                    .snippet("Dipartimento di Design")
            );
        });

    }

    @SuppressWarnings( {"MissingPermission"})
    private void enableLocationComponent() {
        // Check if permissions are enabled and if not request
        if (PermissionsManager.areLocationPermissionsGranted(this)) {
            // Activate the MapboxMap LocationComponent to show user location
            // Adding in LocationComponentOptions is also an optional parameter
            LocationComponent locationComponent = mapboxMap.getLocationComponent();
            locationComponent.activateLocationComponent(this);
            locationComponent.setLocationComponentEnabled(true);
            // Set the component's camera mode
            locationComponent.setCameraMode(CameraMode.TRACKING_GPS);
            locationComponent.setRenderMode(RenderMode.GPS);
            originLocation = locationComponent.getLastKnownLocation();

        } else {
            permissionsManager = new PermissionsManager(this);
            permissionsManager.requestLocationPermissions(this);
        }
    }

    @Override
    public void onMapClick(@NonNull LatLng point){

        if (destinationMarker != null) {
            mapboxMap.removeMarker(destinationMarker);
        }
        destinationCoord = point;
        destinationMarker = mapboxMap.addMarker(new MarkerOptions()
                .position(destinationCoord)
        );

            destinationPosition = Point.fromLngLat(destinationCoord.getLongitude(), destinationCoord.getLatitude());
            originPosition = Point.fromLngLat(originCoord.getLongitude(), originCoord.getLatitude());
            getRoute(originPosition, destinationPosition);
    }




    private void getRoute(Point origin, Point destination) {

        Toast.makeText(this, "Calculating route...", Toast.LENGTH_SHORT).show();

        MapboxDirections.builder()
                .origin(origin)
                .destination(destination)
                .accessToken(getString(R.string.access_token))
                .profile(DirectionsCriteria.PROFILE_CYCLING)
                .bannerInstructions(true)
                .steps(true)
                .roundaboutExits(true)
                .build();

        NavigationRoute.builder(this)
                .accessToken(getString(R.string.access_token))
                .origin(origin)
                .destination(destination)
                .profile(DirectionsCriteria.PROFILE_CYCLING)
                .voiceUnits("metric")
                .build()
                .getRoute(new Callback<DirectionsResponse>() {
                    @Override
                    public void onResponse(Call<DirectionsResponse> call, Response<DirectionsResponse> response) {
                        // You can get the generic HTTP info about the response
                        Log.d(TAG, "Response code: " + response.code());
                        if (response.body() == null) {
                            Log.e(TAG, "No routes found, make sure you set the right user and access token.");
                            return;
                        } else if (response.body().routes().size() < 1) {
                            Log.e(TAG, "No routes found");
                            return;
                        }

                        currentRoute = response.body().routes().get(0);

                        // Draw the route on the map
                        if (navigationMapRoute != null) {
                            navigationMapRoute.removeRoute();
                        } else {
                            navigationMapRoute = new NavigationMapRoute(null, mapView, mapboxMap, R.style.NavigationMapRoute);

                            startButton.setVisibility(View.VISIBLE);
                            startButton.setBackgroundResource(R.color.colorPrimary);

                        }
                        navigationMapRoute.addRoute(currentRoute);
                    }

                    @Override
                    public void onFailure(Call<DirectionsResponse> call, Throwable throwable) {
                        Log.e(TAG, "Error: " + throwable.getMessage());
                    }
                });
    }



    @Override
    public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
        permissionsManager.onRequestPermissionsResult(requestCode, permissions, grantResults);
    }

    @Override
    public void onExplanationNeeded(List<String> permissionsToExplain) {
        Toast.makeText(this, R.string.user_location_permission_explanation, Toast.LENGTH_LONG).show();
    }

    @Override
    public void onPermissionResult(boolean granted) {
        if (granted) {
            enableLocationComponent();
        } else {
            Toast.makeText(this, R.string.user_location_permission_not_granted, Toast.LENGTH_LONG).show();
            finish();
        }
    }

    @Override
    protected void onStart() {
        super.onStart();
        mapView.onStart();
        Intent enableBtIntent = new Intent(BluetoothAdapter.ACTION_REQUEST_ENABLE);
        int REQUEST_ENABLE_BT = 1;
        this.startActivityForResult(enableBtIntent, REQUEST_ENABLE_BT);
    }

    @Override
    protected void onResume() {
        super.onResume();
        mapView.onResume();
    }

    @Override
    protected void onPause() {
        super.onPause();
        mapView.onPause();
        }

    @Override
    protected void onStop() {
        super.onStop();
        mapView.onStop();
    }


    @Override
    public void onSaveInstanceState(@NonNull Bundle outState) {
        super.onSaveInstanceState(outState);
    }

    @Override
    protected void onRestoreInstanceState(Bundle savedInstanceState) {
        super.onRestoreInstanceState(savedInstanceState);
    }


    @Override
    protected void onDestroy() {
        super.onDestroy();
        mapView.onDestroy();
        if (disposable != null) {
            disposable.dispose();
            disposable = null;
        }
    }

    @Override
    public void onLowMemory() {
        super.onLowMemory();
        mapView.onLowMemory();
    }


    @Override
    public void onNavigationReady(boolean isRunning) {

        NavigationViewOptions options = NavigationViewOptions.builder()
                .directionsRoute(currentRoute)
                .shouldSimulateRoute(true)
                .bannerInstructionsListener(this)
                .milestoneEventListener(MainActivity.this)
                .build();

        navigationView.startNavigation(options);

    }

    @Override
    public void onProgressChange(Location location, RouteProgress routeProgress) {
        Toast.makeText(this, "change", Toast.LENGTH_LONG).show();
    }

    @Override
    public void userOffRoute(Location location) {
        Toast.makeText(this, "offRoute", Toast.LENGTH_LONG).show();
    }

    @Override
    public void onMilestoneEvent(RouteProgress routeProgress, String instruction, Milestone milestone) {

        Context context = getApplicationContext();
        String instr = myInstruction.buildInstruction(routeProgress);
        int duration = Toast.LENGTH_SHORT;

        Toast.makeText(context, instr, duration).show();
    }

    Instruction myInstruction = new Instruction() {

        @Override
        public String buildInstruction(RouteProgress routeProgress) {
            return routeProgress.currentLegProgress().upComingStep().maneuver().type()+" "+routeProgress.currentLegProgress().upComingStep().maneuver().modifier();
            }

        };

    @Override
    public BannerInstructions willDisplay(BannerInstructions instructions) {

        RxBleClient rxBleClient = RxBleClient.create(this);

        Context context = getApplicationContext();
        String instr = instructions.primary().type()+" "+instructions.primary().modifier();

        int duration = Toast.LENGTH_SHORT;

//        Toast.makeText(context, instr, duration).show();

        device = rxBleClient.getBleDevice(TEST_DEVICE_ADDRESS);

        Disposable disposable = device.establishConnection(true) // <-- autoConnect flag
                .flatMapSingle(rxBleConnection -> rxBleConnection.writeCharacteristic(WRITE_CHARACTERISTIC, instr.getBytes()))
                .subscribe(
                        characteristicValue -> {
                            // Characteristic value confirmed.
                        },
                        throwable -> {
                            // Handle an error here.
                        }
                );

        // When done... dispose and forget about connection teardown :)
//        disposable.dispose();

        return instructions;
    }
}

What am I doing wrong?

UPDATE: in lack of a better solution I found a workaround: using onProgressChange listener I use the routeProgress object to get all the data i need to show the instruction at the right time and just once per step.

@Override
    public void onProgressChange(Location location, RouteProgress routeProgress) {
        if (routeProgress.currentLegProgress().currentStepProgress().distanceRemaining()>20) {
            noRe = false;
        }

            Context context = getApplicationContext();
        String instr = myInstruction.buildInstruction(routeProgress).replaceAll(" ", "");
        int duration = Toast.LENGTH_SHORT;

        if (instr.length() < 20 &&
                !noRe &&
                routeProgress.currentLegProgress().currentStepProgress().distanceRemaining() < activationDistance
                ) {
            noRe = true;
            Toast.makeText(context, instr, duration).show();
            }
    }
NoveB
  • 11
  • 3
  • Please do not use screen shots to post your code. You should also have a look at [ask] and [mcve]. – Barns Dec 13 '18 at 19:21
  • Sorry! I just worried about wiriting a concise post to make it more readable...I hope it's ok now! – NoveB Dec 13 '18 at 23:23

1 Answers1

0

thanks for checking out the Navigation SDK!

I think the issue here is the identifier you're using for your StepMilestone. We currently use 1 for our VoiceInstructionMilestone. Can you set it to something else like 10 or 20? Thanks!

After looking at your updated code, it looks like the issue is you are creating two navigation scenarios and adding your Milestone to the one that isn't being used.

MapboxNavigation is never being used (from what I can see) in the code above. NavigationLauncher creates a new instance "under-the-hood" for the turn-by-turn UI. If you want to use the turn-by-turn-UI with custom Milestones, you need to use NavigationViewOptions#milestones with the NavigationView. See here for this setup. Thanks!

Dan Nesfeder
  • 298
  • 1
  • 6
  • I tried to put 10 and 20 but it didn't work in both cases. I updated the question with a little more information...I hope it's clearer now! – NoveB Dec 13 '18 at 23:24
  • I updated my comment after looking at your updated code. – Dan Nesfeder Dec 17 '18 at 16:43
  • thanks again for the reply! I managed to trigger the milestone with the NavigationView! Now it outputs the the upcoming step maneuver. The problem is that it's firing just the default milestones (roughly at the beginning of the road and mid-last part) but not the custom one. To check that, I tried setting STEP_DISTANCE_REMAINING_METERS to 1 to easily see if the milestone was getting triggered just at the maneuver but it didn't seem to work. I also found [link](https://i.ibb.co/hL6t8fY/milestone-List.png) in the debugger. I'm not sure but maybe i'm supposed to have the custom milestone listed. – NoveB Dec 19 '18 at 00:47
  • Also, is there a way to remove the default milestones and get just the custom one? I updated the code with the full java file! – NoveB Dec 19 '18 at 01:01
  • Just found a workaround to activate the instruction at the right time. I updated the main post with the details. – NoveB Dec 27 '18 at 13:24