I have a MapView that I'm displaying a "useful radius" (think accuracy of coordinate) in. Using MapView's Projection's metersToEquatorPixels, which is admittedly just for equatorial distance) isn't giving me an accurate enough distance (in pixels). How would you compute this if you wanted to display a circle around your coordinate, given radius?
So, Google Maps uses a Mercator projection. This means that the further you get from the equator the more distorted the distances become. According to this discussion, the proper way to convert for distances is something like:
public static int metersToRadius(float meters, MapView map, double latitude) {
return (int) (map.getProjection().metersToEquatorPixels(meters) * (1/ Math.cos(Math.toRadians(latitude))));
}
mMap.addCircle(
new CircleOptions().center(
new LatLng(
bounds.getCenter().latitude,
bounds.getCenter().longitude
)
)
.radius(50000)
.strokeWidth(0f)
.fillColor(0x550000FF)
);
projection.toPixels(GeoP, pt);
float radius = projection.metersToEquatorPixels(50);
Try this and see... i used it on my MapRadius and it seems to be working
Related
I want to create android application "Points of interest". I've read many different tutorials, and I do not understand why I need to convert GPS coordinates to ECEF and then to ENU. Can you explain, please?
Thanks!
Geospatial coordinate systems are a big topic, but the main choice between systems such as ECEF and ENU is about whether you want to describe a large region of the Earth's surface or just a small region.
When Android provides a geolocation via the LocationListener API, it typically does this using latitude/longitude/altitude which is ideal for representing any point over the Earth's surface, but is a "polar" or "geodetic" coordinate system that isn't ideal for plotting 2D locations. Standard techniques allow this coordinate system to be converted into ECEF, which is another coordinate system which is suitable for the whole globe, but is "cartesian" so can be rotated and scaled using much simpler mathematical operations than the original latitude/longitude/altitude coordinates.
Earth-Centred Earth Fixed (ECEF) uses a coordinate system with its origin at the Earth's centre, so that any point on the ground will have coordinate values that are typically in the millions of metres. This is great for describing satellite orbits, or locations that span multiple continents, but not very convenient for 2D plots of points of interest within a town or city.
If you want to draw a 2D map of a small region of the Earth's surface, then the East-North-Up coordinate system may be much more convenient. To use this, you need a reference location (such as the centre of a particular city) about which the local East/North/Up directions can be defined. Those then provide a set of x/y/z axes, where the x & y axes might be directly converted into the 2D screen coordinates. Obviously, as the region of interest grows larger (e.g. more than 100km), the effects of the Earth's curvature become more noticeable, and an ENU coordinate system will be less useful. See wikipedia for more info.
Moving from an ECEF to ENU coordinate system can be done by a simple set of matrix additions & multiplications which can be computed from the ECEF location of the centre of the map, and the unit vectors in the east/north/up directions.
You can do it this way on Java
public List<Double> convertGpsToECEF(double lat, double longi, float alt) {
double a=6378.1;
double b=6356.8;
double N;
double e= 1-(Math.pow(b, 2)/Math.pow(a, 2));
N= a/(Math.sqrt(1.0-(e*Math.pow(Math.sin(Math.toRadians(lat)), 2))));
double cosLatRad=Math.cos(Math.toRadians(lat));
double cosLongiRad=Math.cos(Math.toRadians(longi));
double sinLatRad=Math.sin(Math.toRadians(lat));
double sinLongiRad=Math.sin(Math.toRadians(longi));
double x =(N+0.001*alt)*cosLatRad*cosLongiRad;
double y =(N+0.001*alt)*cosLatRad*sinLongiRad;
double z =((Math.pow(b, 2)/Math.pow(a, 2))*N+0.001*alt)*sinLatRad;
List<Double> ecef= new ArrayList<>();
ecef.add(x);
ecef.add(y);
ecef.add(z);
return ecef;
}
public List<Double> convertECEFtoENU(List<Double> ecefUser, List<Double> ecefPOI, double lat, double longi){
double cosLatRad=Math.cos(Math.toRadians(lat));
double cosLongiRad=Math.cos(Math.toRadians(longi));
double sinLatRad=Math.sin(Math.toRadians(lat));
double sinLongiRad=Math.sin(Math.toRadians(longi));
List<Double> vector = new ArrayList<>();
vector.add(ecefUser.get(0)-ecefPOI.get(0));
vector.add(ecefUser.get(1)-ecefPOI.get(1));
vector.add(ecefUser.get(2)-ecefPOI.get(2));
double e= vector.get(0)*(-sinLongiRad)+vector.get(0)*(cosLongiRad);
double n= vector.get(0)*(-sinLatRad)*(cosLongiRad)+vector.get(1)*(-sinLatRad)*(sinLongiRad)+vector.get(2)*cosLatRad;
double u= vector.get(0)*(cosLatRad)*(cosLongiRad)+vector.get(1)*(cosLatRad)*(sinLongiRad)+vector.get(2)*sinLatRad;
List<Double> enu= new ArrayList<>();
enu.add(e);
enu.add(n);
enu.add(u);
return enu;
}
Suppose if a marker's longitude is 124.4567. How can I calculate the longitude which is 1 centimetre away to the left side of the screen? It will vary depending on the screen density and zoom level. Is there any inbuilt method to calculate that longitude?
PS: I am sorry, it was "longitude". I always am confused by the two. I have edited the question.
I am not trying to calculate geological distances between two markers. Basically, I want to know how much longitude is 1 centimetre on the screen (not 1 centimetre of actual land) of the device. I mean, 1cm on the screen could be 30 degree longitude difference if I have zoom it out on my phone, but 1cm on the screen could be 1 degree longitude on your phone if you have zoomed it in.
You got distance calculation between 2 coordinator from here:
Distance betwee
n coordinator
And you have destination long, source long, source lat, and distance just find out destination lat.
And about zoom level, you have scale ratio of google map here:
Google map scale ratio
"To the left side" means western direction. Actually, latitude varies from -90 to 90 degrees, so there is no latitude 124.
If you have any point LatLng, you can find point 1 meter to the left via simple math.
Radius of a parallel ring is r = R * cos(latitude_in_radians), so 1 meter takes 360 / r of longitude.
Therefore you can calculate your point as follows without any library.
var EarthR = 6378137;
var point = {lat: 45, lng: 124};
var point2 = {lat: point.lat, lng: point.lng - 360 / ( EarthR * Math.cos(point.lat * Math.PI / 180))};
console.log(point2);
After a lot of Google search, I have found this How to access Google Maps API v3 marker's DIV and its pixel position?. The answer showed how to convert screen locations to lat/lng coordinates. So I modified it a little to get the position 1 centimetre on the left, no matter what the zoom level is.
I put a marker on the left for visual debugging, when a marker is clicked. Here is the code. I am hoping it could be helpful to future people.
var dpi = resources.displayMetrics.densityDpi;
var pixelsInCm = (dpi/2.54).toInt();
var existingPoint = mMap!!.projection.toScreenLocation(marker.position);
var leftPoint = Point(existingPoint.x - pixelsInCm, existingPoint.y);
var leftLatLng = mMap!!.projection.fromScreenLocation(leftPoint);
//The code below is only for visual test. Not necessary.
var leftMarker = MarkerOptions()
leftMarker.position(leftLatLng);
leftMarker.title("1cm to the left");
mMap!!.addMarker(leftMarker);
I want to perform a zoom on a region in google map
map.animateCamera(CameraUpdateFactory.newLatLngBounds(new LatLngBounds(southwest, northeast), padding)
the problem is that i dont have: southwest and northeast points
I just have the center of the map (LatLnt) and the distance in meters between southwest and northeast,
I'd like to know how to calculate southwest and northeast points from map_center and the distance?
any help is welcome
thank you in advance.
This answer assumes that you are using a square map. In other words that the angle from the starting point is 45 degrees (also assuming that the map is flat which means the distances are small). I am also assuming that you are not at either of the poles (yeah I know duh!).
Ok with that in mind it is simple geometry. Since you know the start point and the length from the Southwest corner to the Northeast corner I use half this distance as that is the distance from the starting point to each of these points. I am then finding the x and y component (which is the same with a square map) of this "half distance". To do this I just use the formula latlongOffset equals half of the "half distance" times the square root of 2.
the code is as follows (you provide a LatLng startPoint, Double someDistance, and int padding):
LatLng southwest, northeast;
Double swLat, neLat, swLng, neLng;
Double halfSomeDistance = someDistance / 2.0;
Double latlongOffset = halfSomeDistance * Math.sqrt(2.0) / 2.0;
swLat = startPoint.latitude - latlongOffset;
swLng = startPoint.longitude - latlongOffset;
neLat = startPoint.latitude + latlongOffset;
neLng = startPoint.longitude + latlongOffset;
southwest = new LatLng(swLat, swLng);
northeast = new LatLng(neLat, neLng);
map.animateCamera(CameraUpdateFactory.newLatLngBounds(new LatLngBounds(southwest, northeast), padding));
If you are not using a square map you will need to use right triangle geometry to get the latOffset and lngOffset as they will not be the same.
I hope this helps.
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.
I want to know how much meters is a certain pixel distance, at a given zoom level.
Reason: I want to know the radius, in meters, of a circle in the mapView, which fits perfectly in the mapView -> radiusPixels = mapView.getWidth()/2;
I found the method mapView.getProjection().metersToEquatorPixels(radiusMeters), which does the opposite of that what I need. But there's no inverse for this method or anything else useful.
My (probably naive) approach to solve it is as follows:
private double getFittingRadiusInMeters() {
return getMeters(mapView.getWidth() / 2);
}
private double getMeters(int pixels) {
Projection proj = mapView.getProjection();
Point mapCenterPixels = new Point(mapView.getWidth() / 2, mapView.getHeight() / 2);
//create 2 geopoints which are at pixels distance
GeoPoint centerGeoPoint = proj.fromPixels(mapCenterPixels.x, mapCenterPixels.y);
GeoPoint otherGeoPoint = proj.fromPixels(mapCenterPixels.x + pixels, mapCenterPixels.y);
Location loc = new Location("");
loc.setLatitude(centerGeoPoint.getLatitudeE6() / 1E6);
loc.setLongitude(centerGeoPoint.getLongitudeE6() / 1E6);
Location loc2 = new Location("");
loc2.setLatitude(otherGeoPoint.getLatitudeE6() / 1E6);
loc2.setLongitude(otherGeoPoint.getLongitudeE6() / 1E6);
return loc.distanceTo(loc2);
}
But it doesn't work well. I always get circles which are far smaller than the mapView - the radius is too small.
I know the distanceTo method says "approximate" but the radius differ significantly from the expected size. Should not be an effect of the approximation.
Thanks.
There is a small mistake in your approach.
You are calculating the value for screen half with at screen center level. The distance found is only valid to draw a circle at the same Latitude value (the Longitude may change without problem).
Because earth is shperic, the distance for the same number of pixels calculated at different Latitude levels, produces different results. Moving from Equador level to position closer to Pole level, same number of pixels result a in smaller distance in meters.
However, this will only be noticable if you call getFittingRadiusInMeters() with map positioned in a very distant Latitude from where you draw the circle.
Otherwise, it should work fine.
Solution
The method getMeters() should receive as parameter a GeoPoint (or at least the Latitude) that should be used to calculate the distance.
Regards.