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):
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:
- decide how to determine "main" polyline (longest or having largest amount of points or something else) that should be shown on top of others;
- 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:
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:
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
Firebase Realtime Database Search by Word in Between the Query
How to Use Tablayout with Viewpager2 in Android
Android Runtime Permissions- How to Implement
Recyclerview Painfully Slow to Load Cached Images Form Picasso
How to Create Button Dynamically in Android
How to Ask Permission to Access Gallery on Android M.
How Does One Configure Rjava on Osx to Select the Right Jvm -- .Jinit() Failing
Alternative to Jzebra/Qz Java Raw Print Plugin After Npapi Being Dropped on Chrome Browser
Why Does a Try/Catch Block Create New Variable Scope
How to Apply Custom Filters in a Camera [Surfaceview Preview]...
Android Intent for Sending Email with Attachment
Apache Http Client or Urlconnection
Resources.Openrawresource() Issue Android
Libjvm.So: Cannot Open Shared Object File: No Such File or Directory
Converting a Java Arraylist of Strings to a JavaScript Array