Polyline Is Not on the Roads: It Goes Straight from One Point to Other

Polyline between two locations not snapped to road

Okay, if anyone experiences this issue or similar, I've managed to fix this and it seems the way to correctly plot a polyline on a map that follows the smooth contours of the road is to use each individual polyline > point encoded string within each steps object. It also seems I misread the documentation that clearly states the polyline encoded string in each steps object provides an approximate smoothed path of each step, which I'd imagine others may also have missed.

Basically, the overview_polyline encoded string will only provide an overview of the route and thus not provide a smooth path between locations. It may be perfect for routes that consist of largely straight lines (not great in the UK obviously), so each individual polyline encoded string is what is needed. I've just tested this and it works perfectly, without the need at all for the Snap to Roads API.

My altered code to fix the problem in my use case is as follows:

    StringRequest stringRequest = new StringRequest(Request.Method.GET, "https://maps.googleapis.com/maps/api/directions/json?origin=START_LOCATION&destination=END_LOCATION&key=API_KEY",
new Response.Listener<String>() {
@Override
public void onResponse(String response) {

JSONObject directions;
try {
directions = new JSONObject(response);

JSONArray routes = directions.getJSONArray("routes");

mCoordinates = new ArrayList<>();
for (int a = 0; a < routes.length(); a++) {
JSONObject routesObject = routes.getJSONObject(a);

JSONArray legsArray = routesObject.getJSONArray("legs");
for (int b = 0; b < legsArray.length(); b++) {
JSONObject legsObject = legsArray.getJSONObject(b);
JSONArray steps = legsObject.getJSONArray("steps");
for (int c = 0; c < steps.length(); c++) {
JSONObject stepsObject = steps.getJSONObject(c);
JSONObject polyLineObject = stepsObject.getJSONObject("polyline");
mCoordinates.addAll(PolyUtil.decode(polyLineObject.getString("points")));
}
}

PolylineOptions routeCoordinates = new PolylineOptions();
for (LatLng latLng : mCoordinates) {
routeCoordinates.add(new LatLng(latLng.latitude, latLng.longitude));
}
routeCoordinates.width(5);
routeCoordinates.color(Color.BLUE);

Polyline route = mGoogleMap.addPolyline(routeCoordinates);

}

} catch (JSONException e) {
e.printStackTrace();
}

}
}, new Response.ErrorListener() {
@Override
public void onErrorResponse(VolleyError error) {
// TODO handle error
}

});

Roads API snapped polyline just a straight line

i just found out that the points(lat,lng) requested should not have a long distance between them. snapped roads is obtained when points are tight.

map v2 drawed polylines are not exactly on the road

You are using the overview_polyline from the returned routes object to draw your path. As the documentation says (emphasis mine)

overview_polyline contains a single points object that holds an encoded polyline representation of the route. This polyline is an approximate (smoothed) path of the resulting directions.

You will get a much better approximation if you draw the polyline object of each step that you receive in the legs array.

From your example (the red line represents the overview_polyline and the blue line represents the polyline object of the first step received):

Sample Image

Find out the sections where polylines overlap GoogleMap

Easiest way (in case you have not so many polylines) is to use Google Maps Roads API part Snap to Road which

returns the best-fit road geometry for a given set of GPS coordinates.
This service takes up to 100 GPS points collected along a route, and
returns a similar set of data with the points snapped to the most
likely roads the vehicle was traveling along.

like in this example:

public class MainActivity extends AppCompatActivity implements OnMapReadyCallback {

private GoogleMap mGoogleMap;
private MapFragment mapFragment;

@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);
}

@Override
public void onMapReady(GoogleMap googleMap) {
mGoogleMap = googleMap;

List<LatLng> sourcePoints = new ArrayList<>();
sourcePoints.add(new LatLng(<COORDINATES_OF_POLYLINE>));
...
sourcePoints.add(new LatLng(<COORDINATES_OF_POLYLINE>));

PolylineOptions polyLineOptions = new PolylineOptions();
polyLineOptions.addAll(sourcePoints);
polyLineOptions.width(5);
polyLineOptions.color(Color.BLUE);
mGoogleMap.addPolyline(polyLineOptions);

mGoogleMap.animateCamera(CameraUpdateFactory.newLatLngZoom(sourcePoints.get(0), 15));

List<LatLng> snappedPoints = new ArrayList<>();
new GetSnappedPointsAsyncTask().execute(sourcePoints, null, snappedPoints);
}

private String buildRequestUrl(List<LatLng> trackPoints) {
StringBuilder url = new StringBuilder();
url.append("https://roads.googleapis.com/v1/snapToRoads?path=");

for (LatLng trackPoint : trackPoints) {
url.append(String.format("%8.5f", trackPoint.latitude));
url.append(",");
url.append(String.format("%8.5f", trackPoint.longitude));
url.append("|");
}
url.delete(url.length() - 1, url.length());
url.append("&interpolate=true");
url.append(String.format("&key=%s", <your_Google_Maps_API_key>);

return url.toString();
}

private class GetSnappedPointsAsyncTask extends AsyncTask<List<LatLng>, Void, List<LatLng>> {

protected void onPreExecute() {
super.onPreExecute();
}

protected List<LatLng> doInBackground(List<LatLng>... params) {

List<LatLng> snappedPoints = new ArrayList<>();

HttpURLConnection connection = null;
BufferedReader reader = null;

try {
URL url = new URL(buildRequestUrl(params[0]));
connection = (HttpURLConnection) url.openConnection();
connection.setRequestMethod("GET");
connection.connect();

InputStream stream = connection.getInputStream();

reader = new BufferedReader(new InputStreamReader(stream));
StringBuilder jsonStringBuilder = new StringBuilder();

StringBuffer buffer = new StringBuffer();
String line = "";

while ((line = reader.readLine()) != null) {
buffer.append(line+"\n");
jsonStringBuilder.append(line);
jsonStringBuilder.append("\n");
}

JSONObject jsonObject = new JSONObject(jsonStringBuilder.toString());
JSONArray snappedPointsArr = jsonObject.getJSONArray("snappedPoints");

for (int i = 0; i < snappedPointsArr.length(); i++) {
JSONObject snappedPointLocation = ((JSONObject) (snappedPointsArr.get(i))).getJSONObject("location");
double lattitude = snappedPointLocation.getDouble("latitude");
double longitude = snappedPointLocation.getDouble("longitude");
snappedPoints.add(new LatLng(lattitude, longitude));
}

} catch (MalformedURLException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
} catch (JSONException e) {
e.printStackTrace();
} finally {
if (connection != null) {
connection.disconnect();
}
try {
if (reader != null) {
reader.close();
}
} catch (IOException e) {
e.printStackTrace();
}
}

return snappedPoints;
}

@Override
protected void onPostExecute(List<LatLng> result) {
super.onPostExecute(result);

PolylineOptions polyLineOptions = new PolylineOptions();
polyLineOptions.addAll(result);
polyLineOptions.width(5);
polyLineOptions.color(Color.RED);
mGoogleMap.addPolyline(polyLineOptions);

LatLngBounds.Builder builder = new LatLngBounds.Builder();
builder.include(result.get(0));
builder.include(result.get(result.size()-1));
LatLngBounds bounds = builder.build();
mGoogleMap.animateCamera(CameraUpdateFactory.newLatLngBounds(bounds, 10));

}
}

}

(for this you need to add Google Maps Roads API support for your project in Google Cloud Console)

After you process all of the "initial" polylines with Snap To Road all of them should overlaps perfectly and you can use Z-Index property of Polyline to manage Z-order of polylines.

Other way is to detect overlapping of polylines "manually". In that case you should:

  1. decide how to determine "main" polyline (longest or having largest amount of points or something else) that should be shown on top of others;
  2. for each point of "non-main" polyline check is it on "main" polyline or not (you can use isLocationOnPath() method of PolyUtil library) and choose the color for the segment depending on check result: if point on the "main" path (within a specified tolerance in meters) you should not to show segment of "non-main" polyline or show it with selected color otherwise.

Flutter : polylines show straight line

To get the route from point A to point B you will need to use Directions API that is available on the google_maps_webservice flutter package, which is a service from Google Maps Platform that gives the route information

One of the route information is the overview_polyline that holds an encoded polyline representation of the route.

You can get the overview_polyline by having a function that sends request to Directions API using the google_maps_webservice package like this:

import 'package:google_maps_webservice/directions.dart' as directions;

final _directions = new directions.GoogleMapsDirections(apiKey: "YOUR_API_KEY");

var overviewPolylines;

directions.DirectionsResponse dResponse = await _directions.directionsWithAddress(
_originAddress,
_destinationAddress,
);

if (dResponse.isOkay) {
for (var r in dResponse.routes) {
overviewPolylines = r.overviewPolyline.points
}
}

Then, once you get the overview_polyline from Directions API using the sample code above, you will need to decode it using the PolyUtil(); method from the google_maps_util flutter package like this:

import 'package:google_maps_util/google_maps_util.dart';

PolyUtil myPoints = PolyUtil();
var pointArray = myPoints.decode(overviewPolylines);

Once decoded you can pass the pointArray to your polyline object like this:

_polyline.add(Polyline(
color: Colors.blue,
visible: true,
points: pointArray,
polylineId: PolylineId("distance"),
));

How to update marker using live location along a polyline?

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.

How to make Polyline / follow roads in react-native-maps with HERE API

Please add an additional parameter to the calculateroute rest api:
legAttributes=shape

Then the rest api will return shape object containing the points along the roads.

https://route.api.here.com/routing/7.2/calculateroute.json?app_id={myappid}&app_code={myappcode}&waypoint0=geo!${from_lat},${from_long}&waypoint1=geo!${to_lat},${to_long}&mode=fastest;bicycle;traffic:disabled&legAttributes=shape

Please see legAttributes in the documentation.
https://developer.here.com/documentation/routing/topics/resource-calculate-route.html



Related Topics



Leave a reply



Submit