2

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.

Sanoop Surendran
  • 3,484
  • 4
  • 28
  • 49
  • Are you using FINE_LOCATION as permission? – Manoj Perumarath Feb 27 '19 at 10:55
  • 1
    Try to use [Snap to Road](https://developers.google.com/maps/documentation/roads/snap) from [Roads API](https://developers.google.com/maps/documentation/roads/intro) and take a look at [this](https://stackoverflow.com/a/47329553/6950238) answer for example. – Andrii Omelchenko Feb 27 '19 at 11:16
  • @ManojPerumarath Yes, I have pretty much done everything configuring the Location request to receive the highest precision values. – Sanoop Surendran Feb 27 '19 at 11:58
  • @AndriiOmelchenko Yes, i have checked it, but the problem is i have to make the marker to move along the polyline. The polyline am drawing is exact and it is not an issue. The link suggests to something different from my requirement,. – Sanoop Surendran Feb 27 '19 at 12:01
  • 1
    Do you mean [`SphericalUtil.interpolate()`](http://googlemaps.github.io/android-maps-utils/javadoc/com/google/maps/android/SphericalUtil.html#interpolate-LatLng-LatLng-double-)? I mead use `interpolate()` to calculate coordinates of path part? – Andrii Omelchenko Feb 27 '19 at 12:07
  • @AndriiOmelchenko Best example would be Uber, as in the marker on their app doesnt deviate from the polyline, it always stays along the polyline – Sanoop Surendran Feb 27 '19 at 12:31
  • This answer helpful me it is working in flutter 100% see this: https://stackoverflow.com/a/73684671/5579748 – rajesh dabhi Sep 30 '22 at 11:54

2 Answers2

5

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.

Andrii Omelchenko
  • 13,183
  • 12
  • 43
  • 79
  • Sorry for replying late, was carried away with some other work, and yes, will surely try this and will let you know. Thanks :) – Sanoop Surendran Mar 04 '19 at 10:00
  • Just one doubt though, is the marker that gets snapped to the nearest segment customizable? The reason am asking is because, during the starting to the route the location of the car might not be near the polyline, the car might need to travel about 500meter or 1kilometer just to reach the route path (polyline) – Sanoop Surendran Mar 04 '19 at 10:04
  • @Sanoop `30` in `PolyUtil.isLocationOnPath(carPos, segment, true, 30);` line is maximum distance (in meters) for detecting "nearest" segment. So, "nearest" segment will be found if current car position is closer than 30 meters to path. polyline. In other case there is o projection to any polyline segment. – Andrii Omelchenko Mar 04 '19 at 10:34
  • Seems like the solution worked for me. Thanks a lot :) – Sanoop Surendran Mar 07 '19 at 09:13
  • 2
    after hours of trying to implement SphericalUtil.interpolate() solution, I found that the "fraction" is the distance between segment start and current position divided by segment length. It works better than solution with map projection, but it not always helps to avoid issue with parallel roads, because if isLocationOnPath() method returns 'true' for a wrong segment, interpolation will still work incorrectly. The solution is to use smaller tolerance for isLocationOnPath() method – D.Zotov Nov 21 '19 at 09:48
  • This assumes the first segment that you find within that 30 meter tolerance is the closest segment to carPos, which is okay but then when getMarkerProjectionOnSegment is called using that found segment, the marker may be placed outside the segment. I think a solution would be checking that the marker coordinates returned from method are on the actual line segment, or have a better method of determining the actual closest segment, rather than just the first one that's found. I have been having trouble trying to figure that out. – Nalyd Nov 04 '20 at 22:41
  • @Nalyd Of course this is just the idea and first iteration, not optimized fully functional commercial solution. – Andrii Omelchenko Nov 05 '20 at 09:24
  • this is not working properly I have implemented in flutter app with google map and marker set in wrong polyline point segment in route – rajesh dabhi Sep 06 '22 at 10:50
  • @rajeshdabhi Please use solution from D.Zotov comment. – Andrii Omelchenko Sep 06 '22 at 15:16
  • @rajeshdabhi Please how did you go about this on flutter with Google map, I'm not so experienced, it'll save me a lot of time. Thanks in advance – Godwin Mathias Sep 28 '22 at 21:31
  • 1
    @MathiasGodwin I have done in flutter and it is working fine https://stackoverflow.com/a/73684671/5579748 see my answer. – rajesh dabhi Sep 30 '22 at 11:49
1

I know this question is old, but just in case anyone needs it, adding to Andrii Omelchenko's answer, this is one way you could use SphericalUtil.interpolate() to find the point exactly on the segment:

private LatLng getMarkerProjectionOnSegment(LatLng carPos, List<LatLng> segment, Projection projection) {
        Point a = projection.toScreenLocation(segment.get(0));
        Point b = projection.toScreenLocation(segment.get(1));
        Point p = projection.toScreenLocation(carPos);

        if(a.equals(b.x, b.y)) return segment.get(0); // Projected points are the same, segment is very short
        if(p.equals(a.x, a.y) || p.equals(b.x, b.y)) return carPos;

        /*
        If you're interested in the math (d represents point on segment you are trying to find):
        
        angle between 2 vectors = inverse cos of (dotproduct of 2 vectors / product of the magnitudes of each vector)
        angle = arccos(ab.ap/|ab|*|ap|)
        ad magnitude = magnitude of vector ap multiplied by cos of (angle).
        ad = ap*cos(angle) --> basic trig adj = hyp * cos(opp)
        below implementation is just a simplification of these equations
         */

        float dotproduct = ((b.x-a.x)*(p.x-a.x)) + ((b.y-a.y)*(p.y-a.y));
        float absquared = (float) (Math.pow(a.x-b.x, 2) + Math.pow(a.y-b.y, 2)); // Segment magnitude squared

        // Get the fraction for SphericalUtil.interpolate
        float fraction = dotproduct / absquared;

        if(fraction > 1) return segment.get(1);
        if(fraction < 0) return segment.get(0);
        return SphericalUtil.interpolate(segment.get(0), segment.get(1), fraction);
    }
Nalyd
  • 95
  • 1
  • 16