I want to implement a touchListener on a polyline displayed with Google Maps V2 Android API.
Zoom level:
CameraUpdate cameraUpdate = CameraUpdateFactory.newLatLngZoom(lat_Lng, 5);
I tried the following polyline touch code:
boolean onpoly = false;
for (Polyline polyline : mPolylines) {
for (LatLng polyCoords : polyline.getPoints()) {
float[] results = new float[1];
Location.distanceBetween(points.latitude, points.longitude, polyCoords.latitude, polyCoords.longitude, results);
if (results[0] < 100 ) {
onpoly = true;
Marker mark = map.addMarker(new MarkerOptions().position(points).title("AddEvent")
.snippet("" + addressaddexpense).icon(BitmapDescriptorFactory.fromResource(R.drawable.addicon)));
UtilsClass.dropPinEffect(mark);
}// end if..
} // end for loop
}// end for loop
if (onpoly == true) {
Toast.makeText(getActivity(), "Poly line detected", Toast.LENGTH_SHORT).show();
}// end if
It worked but not perfectly.
it will not detect the touch event unless i zoom in, sometimes forcing me to tap the map more than 5 times before zooming to achieve detection.
I then changed the aforementioned if condition from if (results[0] < 100 ) to if (results[0] < 150 * (22 - map.getCameraPosition().zoom)) and the functionality improved but it still does not work all the time.
Note: I want to detect polyline touch at any zoom level.
try this
final List<LatLng> latLngList; // Extract polyline coordinates and put them on this list
private GoogleMap map;
for(int i = 0; i < latLngList.size(); i++){
MarkerOptions mar = new MarkerOptions();
mar.position(new LatLng(latLngList.get(i).latitude, latLngList.get(i).longitude)).icon(BitmapDescriptorFactory.fromResource(R.drawable.trasparent_image)); //this image should be very small in size and transparent
map.addMarker(mar);
}
map.setOnMarkerClickListener(new OnMarkerClickListener() {
#Override
public boolean onMarkerClick(Marker arg0) {
for(int i = 0; i < latLngList.size(); i++){
if(latLngList.get(i).latitude == arg0.getPosition().latitude && latLngList.get(i).longitude == arg0.getPosition().longitude){
Toast.makeText(MainActivity.this, "PolyLineClick ", Toast.LENGTH_SHORT).show();
break;
}
}
return false;
}
});
Until the questions in my comments are answered i thought i'll try to make them redundant by suggesting the usage of a 3rd party library; android-maps-utils
To do what i think you might be trying to do simply integrate the library and use the following line:
PolyUtil.isLocationOnPath(point, polyline.getPoints(), isGeodesic, tolerance);
For more information you can also look into this thread which seems applicable.
Goodluck.
I think your approach is correct. The only thing that fails is the distance check. And this is because os the touch and the zoom level:
You know that when you tap on the screen, the screen point that is passed to the applications is the center of your finger surface, that is in touch with the screen. This means, that even if it seems, that your finger is exactly over the PolyLine, it can be displaced with some pixels...
Now is time for the Zoom level, and depending in its current value, the distance between the point passed to the application and the PolyLine, can vary very much.
As a result, the if clause fails, and you have to tap several times, until some of your taps is near enough to the PolyLine. And of course it gets better with higher zoom level.
You should include the zoom level as you have done in you edited code, but with some extras: Check the "delta" that you will allow to enter the if, but on the max zoom level. It should be a small value. Then you have to just multiply it by the current zoom level, and calculate how this delta changes. Use this zoom dependant value in your if comparison.To enhance it, you can make a more complex calculation and get this delta, starting from pixel distance. Lets say, a tap that at 50px or less to the PolyLine will be accepted. Calculate this pixel distance in meters, again on the max zoom level and use it multiplied by the current zoom...To enhance it even more, you can get this pixel distance, to be dependant on the screen resolution and density.
Here, you can find how to calculate screen pixels to meters: https://stackoverflow.com/a/13635952/4776540
I implemented a similar thing in the following way:
Convert all locations to the screen coordinates using map.getProjection().toScreenLocation()
Use standard distance formula to determine distance from point to point (or from point to segment, if you want to detect clicks on line segments too) and if this distance is less than some threshold - click is detected.
The key point here is to use map projection to get screen coordinates. This way the decision depends on the real pixel distance on the screen, and does not depend on a zoom level.
Related
I'm working on an Android Application using Gooogle Maps API v2. I have markers on my map, and I'd like to circle one of them. I managed to do that easily, by using the Circle and Circle Options classes. But I'd also like my circle to keep the same size on the screen when zooming or unzooming, just like the markers do. It means that the circle must have a constant radius in terms of pixels. Sadly, we cannot set a radius in pixels in the API v2.
I have tried several solutions, but I'm not satisfied.
In the first one, I just multiply or divide the radius :
#Override
public void onCameraChange(CameraPosition position)
{
if(previousZoom > position.zoom) {
mSelectionCircle.setRadius(Math.abs(position.zoom - previousZoom)*2*mSelectionCircle.getRadius());
}
else if(previousZoom < position.zoom) {
mSelectionCircle.setRadius(Math.abs(position.zoom - previousZoom)*mSelectionCircle.getRadius()/2);
}
previousZoom = position.zoom;
}
It seemed to work at first, but produces wrong results when zooming quickly or zooming with fingers. Moreover, the scaling is clearly visible on the screen.
My second solution uses pixel-meter conversions. The idea is to recalculate the radius in meters when zooming/unzooming, so the circle has a constant size on the screen. To do that, I get the current position of the Circle on the screen:
Point p1 = mMap.getProjection().toScreenLocation(mSelectionCircle.getCenter());
Then I create another point which is on the edge of the circle:
Point p2 = new Point(p1.x + radiusInPixels, p1.y);
Where
int radiusInPixels = 40;
After that, I use a function which returns the distance between these two points in meters.
private double convertPixelsToMeters(Point point1, Point point2) {
double angle = Math.acos(Math.sin(point1.x) * Math.sin(point2.x)
+ Math.cos(point1.x) * Math.cos(point2.x) * Math.cos(point1.y- point2.y));
return angle * Math.PI * 6378100.0; // distance in meters
}
6378100 is average Earth radius. Finally, I set the new radius of the Circle :
mSelectionCircle.setRadius(convertPixelsToMeters(p1, p2));
It should work in theory but I get ridiculous radius values (10^7 m!). The conversion function may be wrong?
So is there a simpler method to do that, or if not, may you help me to understand why my second soluton doesn't work?
Thanks!
You probably don't really care about an exact pixel size, just that it looks the same for all zoom levels and device rotations.
Here is a fairly simple way to do this. Draw (and redraw if the zoom is changed) a circle whose radius is some percentage of the diagonal of the visible screen.
The Google Maps API v2 has a getProjection() function that will return the lat/long coordinates of the 4 corners of the visible screen. Then using the super convenient Location class, you can calculate the distance of the diagonal of what is visible on the screen, and use a percentage of that diagonal as the radius of your circle. Your circle will be the same size no matter what the zoom scale is or which way the device is rotated.
Here is the code in Java:
public Circle drawMapCircle(GoogleMap googleMap,LatLng latLng,Circle currentCircle) {
// get 2 of the visible diagonal corners of the map (could also use farRight and nearLeft)
LatLng topLeft = googleMap.getProjection().getVisibleRegion().farLeft;
LatLng bottomRight = googleMap.getProjection().getVisibleRegion().nearRight;
// use the Location class to calculate the distance between the 2 diagonal map points
float results[] = new float[4]; // probably only need 3
Location.distanceBetween(topLeft.latitude,topLeft.longitude,bottomRight.latitude,bottomRight.longitude,results);
float diagonal = results[0];
// use 5% of the diagonal for the radius (gives a 10% circle diameter)
float radius = diagonal / 20;
Circle circle = null;
if (currentCircle != null) {
// change the radius if the circle already exists (result of a zoom change)
circle = currentCircle;
circle.setRadius(radius);
} else {
// draw a new circle
circle = googleMap.addCircle(new CircleOptions()
.center(latLng)
.radius(radius)
.strokeColor(Color.BLACK)
.strokeWidth(2)
.fillColor(Color.LTGRAY));
}
return circle;
}
Use a custom icon for Marker instead. You can create Bitmap and Canvas, draw on the latter and use it as a Marker icon:
new MarkerOptions().icon(BitmapDescriptorFactory.fromBitmap(bitmap))...
EDIT:
My previous answer is no longer valid.
As Jean-Philippe Jodoin brought up, you can simply do that with markers and setting their anchor to 0.5/0.5. It's a way cleaner solution.
Pasting the suggested code snippet here for reference:
marker = mMap.addMarker(new MarkerOptions().position(latlng).anchor(0.5f, 0.5f));
Old answer:
I came accross the same problem and could not find a solution, so I did it myself, I will post in the hope that it is helpful to some other people.
The "marker" approach did not work for me because I wanted circles to be centered on a specific lat/lng, and you cannot do that with a marker: if you set a circle icon for your marker, the circle edge will touch the lat/lng, but the circle will not be centered on the lat/lng.
I created a function to compute what should be the size of the circle in meters given the latitude and the camera zoom level, then added a camera listener on the map to update the size of the circle each time the camera changes zoom level. The result is a circle not changing in size (to the bare eye at least).
Here is my code:
public static double calculateCircleRadiusMeterForMapCircle(final int _targetRadiusDip, final double _circleCenterLatitude,
final float _currentMapZoom) {
//That base value seems to work for computing the meter length of a DIP
final double arbitraryValueForDip = 156000D;
final double oneDipDistance = Math.abs(Math.cos(Math.toRadians(_circleCenterLatitude))) * arbitraryValueForDip / Math.pow(2, _currentMapZoom);
return oneDipDistance * (double) _targetRadiusDip;
}
public void addCircleWithConstantSize(){
final GoogleMap googleMap = ...//Retrieve your GoogleMap object here
//Creating a circle for the example
final CircleOptions co = new CircleOptions();
co.center(new LatLng(0,0));
co.fillColor(Color.BLUE);
final Circle circle = googleMap.addCircle(co);
//Setting a listener on the map camera to monitor when the camera changes
googleMap.setOnCameraMoveListener(new GoogleMap.OnCameraMoveListener() {
#Override
public void onCameraMove() {
//Use the function to calculate the radius
final double radius = calculateCircleRadiusMeterForMapCircle(12, co.getCenter().latitude, googleMap.getCameraPosition().zoom);
//Apply the radius to the circle
circle.setRadius(radius);
}
});
}
As MaciejGórski suggested, it's correct and easy way to go; but if you have a lot of markers in google map, let's say 5k markers for example, it will slow down performance dramatically. Some suggestions to show this matter are:
1) Let search Marker clustering utility of Google android map API.
2) However, Marker clustering maybe not fit completely your purpose. So you can customize it by yourself. Here is the thread discussing about this matter: https://github.com/googlemaps/android-maps-utils/issues/29
I'm sorry, I did not try it, since I found using Polyline satisfies my purpose (display a path).
Hope this help,
Mttdat.
In our application we use google map APIs v1.
I wrote grid-based clusterization for markers (total amount up to few thousands). Everything works fine - good performance, etc...
The only problem is that I calculate grid depending on currently viewed area
private void createCluster2DArray() {
double cwidth = (cachedrightLongitude - cachedleftLongitude) / clustersXnum;
double cheight = (cachedtopLatitude - cachedbottomLatitude) / clustersYnum;
for (int i = 0; i < clustersXnum; i++) {
for (int j = 0; j < clustersYnum; j++) {
Cluster cluster;
if (clusters[i][j] == null) {
cluster = new Cluster();
clusters[i][j] = cluster;
} else {
cluster = clusters[i][j];
cluster.list.clear();
}
//calculate dimensions
cluster.left = cachedleftLongitude + i * cwidth;
cluster.right = cluster.left + cwidth;
cluster.bottom = cachedbottomLatitude + j * cheight;
cluster.top = cluster.bottom + cheight;
cluster.calculateCenter(mMapView);
}
}
}
cachedrightLongitude, cachedrightLongitude, cachedrightLongitude, cachedrightLongitude are borders of device screen area in degrees.
The problem, you can see, is that cluster borders changing every time when user changes visible area (change zoom level, or just slide the screen). This leads to clusters recalculation and markers redistribution over them.
The only solution I see is to create some kind of static screen-independent clusters greed for each zoom level(for example at zoom level 5 size of cluster will be 10milli degrees and at level 6 it will be 2milli degrees, so only border-clusters will dynamicaly change their size and outer borders). Am i right?
Is there any other suggestions?
For android maps API v1 there is a clustering library here: https://github.com/damianflannery/Polaris. This is a fork of Cyril Mottier's Polaris library, but the discussion on pull request suggest it won't be merged back into original. See here. I haven't looked at the source, so I can't tell you if they use grid clustering.
As for your question, I think using static screen-independent cluster grid is the way to go. I'd only suggest changing the values of millidegrees. For zoom level that is different by 1, millidegs should be divided (or multiplied) by 2.
Also note that with latitude you can't use degrees value directly, but you have to push it through a Mercator projection. This is to make grid consist of squares instead of having them look like rectangles with height few times greater than width closer to the north and south poles.
This is basically what I do in Android Maps Extensions for maps API v2.
I assumed 180 degrees grid size on zoom level 0, so 90 degrees on zoom level 1, 45 on 2, etc. and about 85 microdegrees on zoom 21. The value can be changed in the API.
To you the most useful parts of the code from Extensions lib would be: SphericalMercator to convert latitude and some portions from GridClusteringStrategy.
I would like to click a spot on a Google maps v2 android map. If the clicked point intersects a point on a polyline path, then display the polyline. I do not see any documented clickable events for polylines in android. I tried to extend the current Polyline object (marked final)
What other options do I have?
You can use library:
https://github.com/googlemaps/android-maps-utils
And detect clicks to polyline using next method (in OnMapClickListener):
PolyUtil.isLocationOnPath(point, polyline.getPoints(), isGeodesic, tolerance);
With the recent update of the maps api, v8.4, introduces clickable Polyline
As mentioned in the doc:
Use the OnPolylineClickListener to listen to click events on a
clickable polyline. To set this listener on the map, call
googleMap.setOnPolylineClickListener(...). When
a user clicks on a polyline, you will receive an
onPolylineClick(Polyline) callback.
gradle-dependency:
compile 'com.google.android.gms:play-services-maps:8.4.0'
implement callback: GoogleMap.OnPolylineClickListener
initialize Polyline:
Polyline polyline = googleMap.addPolyline(options);
polyline.setClickable(true);
...
receive events
#Override
public void onPolylineClick(Polyline polyline) {
....
}
Happy coding :)
Register an OnMapClickListener. Determine if a given click is on your line yourself. If it is, do whatever it was you wanted to do in this case.
I had a similar issue where I could not process click events on polylines. I was using Xamarin for Android which is C# but the functionality is largely the same as the Android Java Libraries in this case.
In the end, I ended up doing what seemed to be the only option.
This involved processing all of the midpoints of my polylines(of which there were around 1300). On every OnMapClick, I took the LatLng of the click event and performed a distance formula between it and the midpoint of all polylines in the static List<PolylineOptions>. I then attached a map marker to the closest polyline.
From a tap on a polyline, it pops up a marker in about a quarter of a second.
I imagine the implemented marker click events from the Google Maps API work in a similar way.
Here is the for loop that handles finding the closest point to a click.
int i = 0;//create an indexer for the loop
double shortestDist = 100;//set an initial very large dist just to be safe
int myIndex = 0;//set variable that will store the running index of the closest point
foreach (PolylineOptions po in myPolylines) {
var thisDist = Distance (point, midPoint (po.Points [0].Latitude, po.Points [0].Longitude, po.Points [1].Latitude, po.Points [1].Longitude));//calculate distance between point and midpoint of polyline
if (thisDist < shortestDist) {
shortestDist = thisDist;//remember current shortest distance
myIndex = i;//set closest polyline index to current loop iteration
}
i++;
}
I know it isn't the prettiest code but it gets the job done. I didn't see a real answer to this anywhere on the internet so here it is. It could probably be made more efficient by calculating the midpoints beforehand and storing them in an equally sized list and then not having to call the midpoint formula for each polyline on every map click but it works really fast already.
EDIT
I do my testing on a galaxy s3 by the way, so I think it's not too inefficient.
If you are using com.google.android.gms:play-services-maps:8.4.0 then it includes polylines click listener
googleMap.setOnPolylineClickListener(new GoogleMap.OnPolylineClickListener()
{
#Override
public void onPolylineClick(Polyline polyline)
{
//do your work selected polyline
}
});
PolylineOptions line = new PolylineOptions();
Polyline polyline = googleMap.addPolyline(line);
polyline.setClickable(true);
I am trying to rotate map view when the user changes his direction ie if user takes left and right turns it should rotate accordingly.I am rotating map view basing on current location bearing it is rotating correctly but it was jittering.Here is the code which i used for rotation
public void onGPSUpdate(Location location)
{
boolean check=isBetterLocation(location, tempLoc);
tempLoc=location;
if(check){
showLocation(location);
}
}
isBetterLocation method is copied from google docs for better location.
private void showLocation(Location loc){
mRotateView.rotate(-loc.getBearing());
}
I registered a location updates with time interval 0 and min distance of 10 for frequent updates.Here my problem is map view is jittering always,can any one tell me how can I smoothly rotate map view like other applications like waze maps do.Thanks...
are you trying to rotate the map in a smooth way such as by one degree at a time or just have it go from degree A to degree B on location update ?
Something like
while (oldAngle != newAngle)
{
mapView.rotate(newAngle);
// this is where you would decied to add or subtract;
newAngle ++ or -- ;
}
not sure if this would work exactly as the loop would run really quickly so maybe do this as a asynctask and add a pause in there to simulate a smooth rotation.
Double angle = Math.atan2((userstartPoint.getX() - userendPoint.getX()), userstartPoint.getY() - userendPoint.getY());
angle = Math.toDegrees(angle);
map.setRotationAngle(angle);
so basically I get the start point (new location) and then the end point (old location) and do a Math.atan2 on it as you can see. Then convert that to a degree and set it to my map rotation.
Now it does not do a smooth rotation but I don't need that. Here is where you could set up your own stepper for a smooth rotate. Unless the google maps already has one.
As the bearing values of the Location are not very exact and tend to jump a little, you should use a filter for the bearing. For example, keep the last 5 bearing-values in an array and use the average of those values as the bearing to rotate the map to. Or use the filter explained in the SensorEvent docs - it's easier to use and can be tweaked better.
This will smoothen out the rotation of the map resp. keep it more stable.
EDIT:
A version of the low-pass filter:
public static float exponentialSmoothing(float input, float output, float alpha) {
output = output + alpha * (input - output);
return output;
}
use it like so:
final static float ALPHA = 0.33; // values between 0 and 1
float bearing;
// on location/bearing changed:
bearing = exponentialSmoothing(bearing, newBearing, ALPHA);
bearing would be the value to use to actually rotate the map, newBearing would be the bearing you get from every event, and with ALPHA you can control how quickly or slowly the rotation acts to a new orientation by weighting how much of the old and the new bearing is taken into account for the result. A small value weighs the old value higher, a high value weighs the new value higher.
I hope that works out better.
To change the bearing of your map, use the Camera class. You can define a new CameraPosition with the new bearing and tell the camera to move with either GoogleMap.moveCamera or GoogleMap.animateCamera if you want a smooth movement.
I have implemented this in my app. What I basically did is that I took the last and second last LatLng of my path and calculate bearing by using
public static float getRotationAngle(LatLng secondLastLatLng, LatLng lastLatLng)
{
double x1 = secondLastLatLng.latitude;
double y1 = secondLastLatLng.longitude;
double x2 = lastLatLng.latitude;
double y2 = lastLatLng.longitude;
float xDiff = (float) (x2 - x1);
float yDiff = (float) (y2 - y1);
return (float) (Math.atan2(yDiff, xDiff) * 180.0 / Math.PI);
}
Set this angle as bearing to camera position.
Note: Sometimes (rarely) it rotates map to opposite direction. i am looking for it but if anyone got reason do reply.
I'm working on a mapping app that plots pins on a MapView based on a user's query. I'm trying to scale the map to fit all the results pins, but I've run into a seemingly strange situation.
I have two variables set up:
latSpan is the difference between the maximum latitude and minimum latitude of any of the results points
lonSpan is the difference between the maximum longitude and minimum longitude of any of the results points
This method
while ((mapView.getLatitudeSpan()) < latSpan) || (mapView.getLongitudeSpan() < lonSpan)){
mapController.zoomOut();
}//end of while loop
is supposed to zoom out to make sure all the pins fit on the viewable map screen.
But I'm experiencing something rather strange. The results of mapView.getLatitudeSpan() and mapView.getLongitudeSpan() are routinely greater than my latSpan and lonSpan values, so the MapController doesn't zoom out enough.
My map is zoomed in pretty far--level 15 or higher.
As an example, one set of search results gave the following values:
latSpan = 17928
lonSpan = 11636
mapView.getLatitudeSpan() = 21933
mapView.getLongitudeSpan() = 20598
Based on these numbers, you wouldn't think that the MapController would need to zoom out. Yet there are pins plotted both above the top and below the bottom of the screen. I changed my WHILE loop to read
while ((mapView.getLatitudeSpan() - 6000) < latSpan...
and that helps, but the right query will still cause issues.
But the real question is, why is this happening?
I'm not sure why you're code isn't working from the snippet provided. Its possible that you are not converting your latSpan and lonSpan to microDegrees (as shown below) and this would cause some issues.
Also if you're trying to make sure your mapView is showing all of the results, there's not much point trying to determine if it needs to zoom before zooming, just zoom it every time. If it turns out that it doesn't need to zoom then nothing will appear to happen and if it does then it does.
You can set a map up to encompass all of your points and move to the centroid of the points as follows:
GeoPoint max = new GeoPoint(maxLatitude, maxLongitude);
GeoPoint min = new GeoPoint(minLatitude, minLongitude);
int maxLatMicro = max.getLatitudeE6();
int maxLonMicro = max.getLongitudeE6();
int minLatMicro = min.getLatitudeE6();
int minLonMicro = min.getLongitudeE6();
GeoPoint center = new GeoPoint((maxLatMicro+minLatMicro)/2,(maxLonMicro + minLonMicro)/2);
controller.zoomToSpan(maxLatMicro - minLatMicro, maxLonMicro - minLonMicro);
controller.animateTo(center);