Search code examples
androidgoogle-mapsgpslocationgoogle-roads-api

How to update marker using live location along a polyline?


My question titles seems to be an existing one, but here is my complete scenario.

I have an activity for Map based operations, where am drawing a polyline along a road, lets say a route between two locations. Basically the app tracks the users current location (Traveling by car). So till part everything is working, as in, the route is properly shown, device Location API is giving location updates (kindof exact), and also i was able to change the location updates smoothly,

So the issue is, the locations updates are sometimes zig zag, it might not touch the road sometimes, the location updates will be going all over the place.

I have looked into ROAD api also, but am not getting the correct help, even from some previously asked questions.

Will it be possible to make the marker move only along the road?

Any kind of help will be appreciated.


Solution

  • You can snap marker to the path by projection of marker on nearest path segment. Nearest segment you can find via PolyUtil.isLocationOnPath():

    PolyUtil.isLocationOnPath(carPos, segment, true, 30)

    and projections of marker to that segment you can find via converting geodesic spherical coordinates into orthogonal screen coordinates calculating projection orthogonal coordinates and converting it back to spherical (WGS84 LatLng -> Screen x,y -> WGS84 LatLng):

    Point carPosOnScreen = projection.toScreenLocation(carPos);
    Point p1 = projection.toScreenLocation(segment.get(0));
    Point p2 = projection.toScreenLocation(segment.get(1));
    Point carPosOnSegment = new Point();
    
    float denominator = (p2.x - p1.x) * (p2.x - p1.x) + (p2.y - p1.y) * (p2.y - p1.y);
    // p1 and p2 are the same
    if (Math.abs(denominator) <= 1E-10) {
        markerProjection = segment.get(0);
    } else {
        float t = (carPosOnScreen.x * (p2.x - p1.x) - (p2.x - p1.x) * p1.x
                + carPosOnScreen.y * (p2.y - p1.y) - (p2.y - p1.y) * p1.y) / denominator;
        carPosOnSegment.x = (int) (p1.x + (p2.x - p1.x) * t);
        carPosOnSegment.y = (int) (p1.y + (p2.y - p1.y) * t);
        markerProjection = projection.fromScreenLocation(carPosOnSegment);
    }
    

    With full source code:

    public class MainActivity extends AppCompatActivity implements OnMapReadyCallback {
    
        private GoogleMap mGoogleMap;
        private MapFragment mapFragment;
    
        private Button mButton;
    
        @Override
        protected void onCreate(Bundle savedInstanceState) {
            super.onCreate(savedInstanceState);
            setContentView(R.layout.activity_main);
    
            mapFragment = (MapFragment) getFragmentManager()
                    .findFragmentById(R.id.map_fragment);
            mapFragment.getMapAsync(this);
    
            mButton = (Button) findViewById(R.id.button);
            mButton.setOnClickListener(new View.OnClickListener() {
                @Override
                public void onClick(View v) {
    
                }
            });
        }
    
        @Override
        public void onMapReady(GoogleMap googleMap) {
            mGoogleMap = googleMap;
            mGoogleMap.setOnMapLoadedCallback(new GoogleMap.OnMapLoadedCallback() {
                @Override
                public void onMapLoaded() {
                    List<LatLng> sourcePoints = new ArrayList<>();
                    PolylineOptions polyLineOptions;
                    LatLng carPos;
    
                    sourcePoints.add(new LatLng(-35.27801,149.12958));
                    sourcePoints.add(new LatLng(-35.28032,149.12907));
                    sourcePoints.add(new LatLng(-35.28099,149.12929));
                    sourcePoints.add(new LatLng(-35.28144,149.12984));
                    sourcePoints.add(new LatLng(-35.28194,149.13003));
                    sourcePoints.add(new LatLng(-35.28282,149.12956));
                    sourcePoints.add(new LatLng(-35.28302,149.12881));
                    sourcePoints.add(new LatLng(-35.28473,149.12836));
    
                    polyLineOptions = new PolylineOptions();
                    polyLineOptions.addAll(sourcePoints);
                    polyLineOptions.width(10);
                    polyLineOptions.color(Color.BLUE);
                    mGoogleMap.addPolyline(polyLineOptions);
    
                    carPos = new LatLng(-35.281120, 149.129721);
                    addMarker(carPos);
                    mGoogleMap.moveCamera(CameraUpdateFactory.newLatLngZoom(sourcePoints.get(0), 15));
    
                    for (int i = 0; i < sourcePoints.size() - 1; i++) {
                        LatLng segmentP1 = sourcePoints.get(i);
                        LatLng segmentP2 = sourcePoints.get(i+1);
                        List<LatLng> segment = new ArrayList<>(2);
                        segment.add(segmentP1);
                        segment.add(segmentP2);
    
                        if (PolyUtil.isLocationOnPath(carPos, segment, true, 30)) {
                            polyLineOptions = new PolylineOptions();
                            polyLineOptions.addAll(segment);
                            polyLineOptions.width(10);
                            polyLineOptions.color(Color.RED);
                            mGoogleMap.addPolyline(polyLineOptions);
                            LatLng snappedToSegment = getMarkerProjectionOnSegment(carPos, segment, mGoogleMap.getProjection());
                            addMarker(snappedToSegment);
                            break;
                        }
                    }
                }
            });
            mGoogleMap.animateCamera(CameraUpdateFactory.newLatLngZoom(sourcePoints.get(0), 15));
        }
    
        private LatLng getMarkerProjectionOnSegment(LatLng carPos, List<LatLng> segment, Projection projection) {
            LatLng markerProjection = null;
    
            Point carPosOnScreen = projection.toScreenLocation(carPos);
            Point p1 = projection.toScreenLocation(segment.get(0));
            Point p2 = projection.toScreenLocation(segment.get(1));
            Point carPosOnSegment = new Point();
    
            float denominator = (p2.x - p1.x) * (p2.x - p1.x) + (p2.y - p1.y) * (p2.y - p1.y);
            // p1 and p2 are the same
            if (Math.abs(denominator) <= 1E-10) {
                markerProjection = segment.get(0);
            } else {
                float t = (carPosOnScreen.x * (p2.x - p1.x) - (p2.x - p1.x) * p1.x
                        + carPosOnScreen.y * (p2.y - p1.y) - (p2.y - p1.y) * p1.y) / denominator;
                carPosOnSegment.x = (int) (p1.x + (p2.x - p1.x) * t);
                carPosOnSegment.y = (int) (p1.y + (p2.y - p1.y) * t);
                markerProjection = projection.fromScreenLocation(carPosOnSegment);
            }    
            return markerProjection;
        }
    
        public void addMarker(LatLng latLng) {
            mGoogleMap.addMarker(new MarkerOptions()
                    .position(latLng)
            );
        }
    }
    

    you'll got something like that:

    Marker snapped to path

    But better way is to calculate car distance from start of the path and find it position on path via SphericalUtil.interpolate() because if several path segments is close one to another (e.g. on different lanes of same road) like that:

    Wrong nearest segment

    to current car position may be closest "wrong" segment. So, calculate distance of the car from the start of the route and use SphericalUtil.interpolate() for determine point exactly on path.