How to change 1 meter to pixel distance? - android

When I develop an Android map application, I want to draw a circle on the map whose radius is 1 meter. As you known, I can't draw 1 meter directly, I should convert 1 meter to the distance of two pixels depend on the zoom level. How to I convert it, is there anything API I can use.
Canvas.draw(x, y, radius), what value should I put to this method ?

Assuming that your map is Google Maps, they use the Mercator projection, so you'd need to use that for the conversion.
Under the Mercator projection, the distance that a pixel represents in meters varies with latitude, so while a meter is a very small distance compared to the Earth radius, latitude is important.
All the examples below are javascript, so you might need to translate them.
Here is a general explanation of the coordinate system:
http://code.google.com/apis/maps/documentation/javascript/maptypes.html#WorldCoordinates
This example contains a MercatorProjection object, which includes the methods fromLatLngToPoint() and fromPointToLatLng():
http://code.google.com/apis/maps/documentation/javascript/examples/map-coordinates.html
Once you have converted your (x,y) to (lat,lon), this is how you draw a circle:
// Pseudo code
var d = radius/6378800; // 6378800 is Earth radius in meters
var lat1 = (PI/180)* centerLat;
var lng1 = (PI/180)* centerLng;
// Go around a circle from 0 to 360 degrees, every 10 degrees
for (var a = 0 ; a < 361 ; a+=10 ) {
var tc = (PI/180)*a;
var y = asin(sin(lat1)*cos(d)+cos(lat1)*sin(d)*cos(tc));
var dlng = atan2(sin(tc)*sin(d)*cos(lat1),cos(d)-sin(lat1)*sin(y));
var x = ((lng1-dlng+PI) % (2*PI)) - PI ;
var lat = y*(180/PI);
var lon = x*(180/PI);
// Convert the lat and lon to pixel (x,y)
}
These two mashups draw a circle of a given radius on the surface of the Earth:
http://maps.forum.nu/gm_sensitive_circle2.html
http://maps.forum.nu/gm_drag_polygon.html
If you choose to ignore the projection then you'd use cartesian coordinates and simply draw the circle using Pythagoras Theorem:
http://en.wikipedia.org/wiki/Circle#Cartesian_coordinates

Take a look at the Projection object in the api. It has a method on it called metersToEquatorPixels(). Given the description in the api, it might only be accurate along the equator, but I thought it was worth mentioning in case accuracy wasn't an issue for you.
Here's the way to use this inside the draw method of your overlay, given the radius in meters and the latitude and longitude of where you want to draw the circle:
Projection projection = mapView.getProjection();
Point center = projection.toPixels(new GeoPoint(yourLat * E6, yourLong * E6), null);
float radius = projection.metersToEquatorPixels(radiusInMeters);
canvas.draw(center.x, center.y, radius, new Paint());

Three questions you got to ask
1- How big is your map
2- What is your zoom level
3- How big is your screen
Let's make the assumption that the map has the same aspect ratio as your screen (if not then you need to worry about which way to crop (verically vs horizontally) or which way to stretch and then change y our answer to 1)
Once you have the answer 1 and 3 you can work out the ratio between meters and pixels in the 100% zoom case, so you will have a pixels per meter
Next you need to maintain a zoom factor (eg: zoom in double size is 200%)
your call to draw the circle will look like this
Canvas.draw(x,y, radius_in_meters * pixels_per_meter * zoom_factor/100);

public static int metersToRadius(float meters, MapView map, double latitude) {
return (int) (map.getProjection().metersToEquatorPixels(meters) * (1/ Math.cos(Math.toRadians(latitude))));
}

You can calculate the zoom level for the radius you want:
First we need to calculate the screen width of the phone.At zoom level 1 the equator of Earth is 256 pixels long and every subsequent zoom level doubles the number of pixels needed to represent earths equator. The following function returns the zoom level where the screen will show an area of 1Km width.
private int calculateZoomLevel() {
int ht, screenWidth;
DisplayMetrics displaymetrics = new DisplayMetrics();
getWindowManager().getDefaultDisplay().getMetrics(displaymetrics);
ht = displaymetrics.heightPixels;
screenWidth = displaymetrics.widthPixels;
double equatorLength = 40075004; // in meters
double widthInPixels = screenWidth;
double metersPerPixel = equatorLength / 256;
int zoomLevel = 1;
while ((metersPerPixel * widthInPixels) > 1000) {
metersPerPixel /= 2;
++zoomLevel;
}
Log.i(tag, "zoom level = " + zoomLevel);
return zoomLevel;
}

Related

Get NW LatLng From Ground Overlay After Rotation

I need to get the NW LatLng from a rectangle bounding a Ground Overlay. Using mOverlay.getBounds() will get me the point I need before rotation (getBounds ignores rotation).
This is the Ground Overlay, with the exact LatLng I need being the point in red. When it sits perfectly North/South (image on the left) I can get the point no problem using getBounds(). After rotating the image, I now need whatever the point is that is the NW corner of the North/South facing rectangle that contains my rotated Ground Overlay (red dot of image on the right).
I had to assume you are using 'bearing' to rotate the overlay image and the anchor of the image is the center. Also, since the map is a modeling a sphere, when you say rectangle we're really assuming a 2d plane tangential to the sphere at the image center (non-rotated).
This explanation makes a simplifying assumption that the rotation angle is less than pi/2 radians and is in the clock-wise direction.
None of this is new so no credit taken - but tried my best to adapt to your problem.
In summary this approach converts the original rectangle (non-rotated) in WGS-84 coordinate system to an x/y coordinate system (centered at origin), computes new x/y of rotated, select corners using trig, derive x/y of upper-left of super-rectangle and translate the result back to WGS-84 spherical coordinate system.
// convert your rotation value (bearing clockwise) to radians
// Using the bounding rectangle (which is of the non-rotated image) compute distance
// between nw and ne corner (width) and nw and sw corner (height) (in meters).
// The purpose of this is to establish an x/y coordinate system with origin being
// the center of the non-rotated image.
// Compute the corner coordinates of original bounding rectangle in an x/y coordinate
// system using the center as the origin (0,0) e.g. divide NW-NE width by 2 change sign as needed. Units are meters
// Compute rotated NW corner (x`,y`) (in x/y system) using original NW corner(x/y)
// and bearing:
// x` = x * cos(bearingInRadians) + y * sin(bearingInRadians) and y` = -(x * sin(bearingInRadians)) + y * cos(bearingInRadians)
// Compute the y-distance from original NW corner (x/y) to new NW corner (x`,y`)
// (subtract the y's)
// Compute latitude of super-bounding by using SphericalUtil.computeOffset using
// original NW lat-lng as 'from', and y-distance (meters) as distance and heading as 0 (north-up).
// Compute the rotated SW corner(x``,y``) (in x/y system) in the same manner
// as the NW corner above.
// Compute the x-distance from original SW corner (x/y) to new SW corner
// Compute longitude of super-bounding rectangle by using
// SphericalUtil.computeOffset using original NW lat-lng as 'from', and
// x-distance (meters) as distance and heading as 270.
Overcoming the simplifications means picking the proper corners to use and which maps to latitude and longitude.
I'd expect something like this has already been implemented but hopefully this helps explain what is needed. Happy hunting.
And here's an implementation of above:
GroundOverlayOptions goo = new GroundOverlayOptions();
BitmapDescriptor bd = BitmapDescriptorFactory.fromResource(R.drawable.rectangle);
goo.image(bd);
goo.position(latLng, 1000F);
GroundOverlay go = mMap.addGroundOverlay(goo);
LatLngBounds llb = go.getBounds();
LatLng ne = llb.northeast;
LatLng sw = llb.southwest;
PolylineOptions po = new PolylineOptions().add(new LatLng(llb.northeast.latitude,llb.southwest.longitude))
.add(llb.northeast)
.add(new LatLng(llb.southwest.latitude,llb.northeast.longitude))
.add(llb.southwest)
.add(new LatLng(llb.northeast.latitude,llb.southwest.longitude));
Polyline polyline = mMap.addPolyline(po);
MarkerOptions mo = new MarkerOptions();
mo.position(new LatLng(ne.latitude,sw.longitude));
mMap.addMarker(mo);
goo.bearing(25.0F);
GroundOverlay go2 = mMap.addGroundOverlay(goo);
double rads = Math.toRadians(25.0);
float[] result = new float[1];
Location.distanceBetween(llb.northeast.latitude, llb.southwest.longitude, llb.northeast.latitude, llb.northeast.longitude, result);
float width = result[0];
Location.distanceBetween(llb.northeast.latitude, llb.northeast.longitude, llb.southwest.latitude, llb.northeast.longitude, result);
float height = result[0];
float upperLeftX = -(width / 2);
float upperLeftY = (height / 2);
float lowerLeftX = upperLeftX;
float lowerLeftY = -upperLeftY;
double newX = (upperLeftX * cos(rads) + upperLeftY * sin(rads));
double newY = (-(upperLeftX * sin(rads)) + upperLeftY * cos(rads));
double deltaY = abs(newY - upperLeftY);
LatLng newLat = SphericalUtil.computeOffset(llb.northeast, deltaY, 0.0);
double newX2 = (lowerLeftX * cos(rads) + lowerLeftY * sin(rads));
double newY2 = (lowerLeftX * Math.sin(rads) + lowerLeftY * cos(rads));
double deltaX = abs(newX2 - lowerLeftX);
LatLng newLng = SphericalUtil.computeOffset(llb.southwest, deltaX, 270.0);
MarkerOptions mo2 = new MarkerOptions();
mo2.position(new LatLng(newLat.latitude, newLng.longitude));
mMap.addMarker(mo2);
And the result:
Notes
There is error introduced when projecting the surface of a sphere using linear scaling but is significantly reduced when working with small areas.
References:
For the trig I referenced: rotate rectangle
Android stuff (1): Spherical Util

How to plot lat long value in terms of pixels in android? Need validation of my approach

I am trying to plot a lat long on my screen using a small circle. This is my code:
currLat = 19.12550467;
currLong = 72.86587704;
collCurrLat = 19.1255857;
collCurrLong = 72.8660916;
cPoint c1 = GlobalMercator.LatLonToPixel(currLat,currLong, 16);
cPoint c2 = GlobalMercator.LatLonToPixel(collCurrLat,collCurrLong, 16);
int dist = GlobalMercator.distanceInMeters(c1.cx, c1.cy, c2.cx, c2.cy, 16);
int xcol = (int) ((((MapView)mParent).getWidth()/360.0) * (180 + collCurrLong));
int ycol = (int) ((((MapView)mParent).getHeight()/180.0) * (90 - collCurrLat));
canvas.drawCircle( xcol, ycol,GlobalMercator.meterDitanceToPixels(10,16 ), mSelectionBrush);
The currLat and currLong are the lat/long of the point at the center of my screen (I plotted it using).
int x = ((MapView)mParent).getWidth()/2;
int y = ((MapView)mParent).getHeight()/2;
The collCurrLat and collCurrLong are the lat/long of the near by point I need to plot on the map.
I used a method distanceInMeters() using these lat/long values to calculate the distance and it comes to be 21 meters.
But when i got the output using drawCircle in the code above, it seems that the x, y coordinates obtained using collCurrLat/collCurrLat are much farther than 21 meters (i.e. the distance between currLat/currLong and collCurrLat/collCurrLong). Also the value for xcol and ycol appear at the same location irrespective of varying lat long values! This is a rendition of my UI.
Can you please validate whether the approach I have taken to plot the collCurrLat/collCurrLong point is correct.
I am not sure it is a good idea to do what you are doing. At least do not use pixels - use dp instead.
public static float convertPixelsToDp(float px, Context context) {
Resources resources = context.getResources();
DisplayMetrics metrics = resources.getDisplayMetrics();
float dp = px / (metrics.densityDpi / 160f);
return dp;
}
More on units of measurment

Animate MapView v2 camera without loosing bearing and tilt

I want to animate MapView camera but want to preserve the bearing and tilt use has set.
I tried below code :
float bearing = map.getCameraPosition().bearing;
float tilt = map.getCameraPosition().tilt;
map.moveCamera(CameraUpdateFactory.newCameraPosition(new CameraPosition.Builder()
.target(new LatLng(
(mapBoundsBuilder.build().northeast.latitude + mapBoundsBuilder
.build().southwest.latitude) / 2,
(mapBoundsBuilder.build().northeast.longitude + mapBoundsBuilder
.build().southwest.longitude) / 2))
.zoom(map.getCameraPosition().zoom).bearing(bearing)
.tilt(tilt).build()));
// Set mapView camera to include all locations in mapBoundsBuilder.
// Set width and height to screen size. Set padding to 50 px,
// duration to 2 sec.
map.animateCamera(CameraUpdateFactory.newLatLngBounds(
mapBoundsBuilder.build(), size.x, mapViewHeight,
CAMERA_PADDING), 2000, null);
But this does not preserve bearing and tilt.
Use a CameraUpdate, which changes only target and zoom:
cameraUpdate = CameraUpdateFactory.newLatLngZoom(newTarget, newZoom);
The remaining question may be how to calculate the new zoom value.
It depends a bit on how you want to treat the different map width in case the map is tilted. But in principle you need the ratio between the current width of the map and the desired width.
With that ratio you can calculate the new zoom value as follows:
(Ratio is greater than 1 if map shows a bigger area than needed and smaller than 1 if it shows a smaller area than needed.)
float zoomIncrement = (float) (Math.log(ratio) / Math.log(2));
float newZoom = map.getCameraPosition().zoom + zoomIncrement;
In my case I am using the smaller of the two distances from nearLeft to nearRight and from nearLeft to farLeft as the map "diameter" and divide this by the diagonal (distance between northeast and southwest) of the LatLngBounds which contains all my locations. As the LatLngBounds is oriented towards north but the map may be turned by e.g. 45 degrees, this ensures, that the locations will always fit into the map:
double innerDiameterOfMap = getInnerDiameterOfMap(map);
double outerDiameterOfBounds = getOuterDiameterOfBounds(bounds);
double ratio = innerDiameterOfMap / outerDiameterOfBounds;
private double getOuterDiameterOfBounds(LatLngBounds bounds) {
return getDistance(bounds.northeast,
bounds.southwest);
}
private double getInnerDiameterOfMap(GoogleMap map) {
double innerDiameterOfMap;
// The diameter of the inner circle is given by the shortest side.
VisibleRegion visibleRegion = map.getProjection().getVisibleRegion();
double side1 = getDistance(visibleRegion.nearLeft,
visibleRegion.nearRight);
double side2 = getDistance(visibleRegion.farLeft,
visibleRegion.nearLeft);
innerDiameterOfMap = Math.min(side1, side2);
return innerDiameterOfMap;
}
For calculating the distance (method getDistance) between two LatLng you will find a lot of solutions in SO.

Is it possible to set dimensions to a groundOverlay that is independent of the map zoom level?

I have a GroundOverlay on my GoogleMap and I want that its dimensions to not change when I zoom in/out on map. Exact like default map markers that always keep their dimensions. I have tried with both forms of the GroundOverlay.setDimensions() but the image is still resize on zoom. Here is my code:
Bitmap btm = BitmapFactory.decodeResource(getResources(), R.drawable.map_arrow);
BitmapDescriptor arrow = BitmapDescriptorFactory.fromBitmap(btm);
float w = btm.getWidth();
float h = btm.getHeight();
if (groundOverlay != null) groundOverlay.remove();
groundOverlay = mMap.addGroundOverlay(new GroundOverlayOptions()
.image(arrow).position(meLoc, w,h).bearing(bearAngle));
groundOverlay.setDimensions(1000);
you have your width and heigth of overlay and placed it on the map according to your zoom level. It seems good for that zoom level. Right? Now you can calculate radius and get meters of your map screen. Because map background overlay width and height values are meters. We have to go with meter not zoom level or anything else. Maybe someone can find a better solution but I have tried too many ways, and end up with this solution and it worked very well.
float zoomLevel=mMap.getCameraPosition().zoom;
//calculate meters*********************
myBounds = mMap.getProjection().getVisibleRegion().latLngBounds;
myCenter= mMap.getCameraPosition().target;
if (myCenter.latitude==0 || myCenter.longitude==0) {
myCenter=new LatLng(myLocation.getLatitude(),myLocation.getLongitude());
}
LatLng ne = myBounds.northeast;
// r = radius of the earth in statute miles
double r = 3963.0;
// Convert lat or lng from decimal degrees into radians (divide by 57.2958)
double lat1 = myCenter.latitude / 57.2958;
double lon1 = myCenter.longitude / 57.2958;
final double lat2 = ne.latitude / 57.2958;
final double lon2 = ne.longitude / 57.2958;
// distance = circle radius from center to Northeast corner of bounds
double dis = r * Math.acos(Math.sin(lat1) * Math.sin(lat2) +
Math.cos(lat1) * Math.cos(lat2) * Math.cos(lon2 - lon1));
//1 Meter = 0.000621371192237334 Miles
double meters_calc=dis/0.000621371192237334;
float factor=1;
if (zoomLevel==15) { // my default zoom level yours can be different
metersoverlay=meters_calc; // global variable metersoverlay set
}
else { // if my zoom level change then I have to calculate dimension scale factor
factor=(float) (meters_calc/metersoverlay);
}
//******************************* now we are ready to set dimension of background overlay
float dimensions=1000*factor;
loadingGroundOverlayBg.setDimensions(dimensions);
I hope it works for all of you :)
I had the same issue. If you want to place an image, you can use the default markers provided by Google maps API. You can set a custom image as the marker icon, which will remain the same size.
private final List<BitmapDescriptor> mImages = new ArrayList<BitmapDescriptor>();
mImages.add(BitmapDescriptorFactory.fromResource(R.drawable.YOUR_IMAGE));
Marker marker = map.addMarker(new MarkerOptions().icon(mImages.get(0)));
This example shows how to set the icon of the marker, from an element of an image list. Hope it helps.

How to: plot a given latitude and longitude into still image map using four (4) coordinates of the image?

I have created this post -> plot a real world lat lon into different angle still image map
from that, I can successfully mark the given lat&lon given the two (2) coordinates (upper left, lower right) but due to incorrect angle of the still image map compare to real world map angle, my mark was displaced.
Now, I am thinking of using four (4) coordinates (upper left, lower left, upper right, lower right) of the image. So that, I could plot the given lat&lon without considering the angle.
I think, even without Android experience could answer this question. I just kinda slow with Mathematics matter.
It is possible to implement it? if yes, any guidance & code snippets are appreciated.
UPDATES1
Main goal is to mark the given lat&lon into image map which has different angle against to real world map.
UPDATES2
I am using the below codes to compute my angle. Would you check it if it is reliable for getting the angle. Then convert it to pixel. NOTE: this codes are using only two coordinates of the image plus target coordinate.
public static double[] calc_xy (double imageSize, Location target, Location upperLeft, Location upperRight) {
double newAngle = -1;
try {
double angle = calc_radian(upperRight.getLongitude(), upperRight.getLatitude(),
upperLeft.getLongitude(), upperLeft.getLatitude(),
target.getLongitude(), target.getLatitude());
newAngle = 180-angle;
} catch (Exception e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
double upperLeft_Target_dist = upperLeft.distanceTo(target);
double upperLeft_Right_dist = upperLeft.distanceTo(upperRight);
double distancePerPx = imageSize /upperLeft_Right_dist;
double distance = upperLeft_Target_dist * distancePerPx;
double radian = newAngle * Math.PI/180;
double[] result = radToPixel(distance, radian);
return result;
}
public static double[] radToPixel(double distance, double radian) {
double[] result = {-1,-1};
result[Functions.Location.PIXEL_X_LON] = distance * Math.cos(radian);
result[Functions.Location.PIXEL_Y_LAT] = distance * Math.sin(radian);
return result;
}
public static double calc_radian(Double x1, Double y1, Double x2, Double y2, Double x3, Double y3)
throws Exception{
double rad = 0.0;
if((Double.compare(x1, x2) == 0 && Double.compare(y1, y2) == 0) ||
(Double.compare(x3, x2) == 0 && Double.compare(y3, y2) == 0))
{
Log.d(tag, "Same place") ;
return rad;
}
/* compute vector */
double BAx = x2 - x1;
double BAy = y2 - y1;
double BCx = x3 - x2;
double BCy = y3 - y2;
double cosA = BAx / Math.sqrt( BAx * BAx + BAy * BAy ) ;
double cosC = BCx / Math.sqrt( BCx * BCx + BCy * BCy ) ;
double radA = Math.acos( cosA ) * 180.0 / Math.PI ;
double radC = Math.acos( cosC ) * 180.0 / Math.PI ;
if( BAy < 0.0 )
{
radA = radA * -1.0 ;
}
if( BCy < 0.0 )
{
radC = radC * -1.0 ;
}
rad = radC - radA ;
if( rad > 180.0 )
{
rad = rad - 360;
}
if( rad < -180.0 )
{
rad = rad + 360;
}
return rad ;
}
This looks like you want to plot the user's current geo-location on an image of, say a building or campus. Assuming this, my approach would be to 'map' the still image to the screen which is likely to require a translation transform, a rotation transform and a scaling transform. In addition, you will need to know the actual geo-location coordinates of at least two points on your image. Given the image in your previous post, I would assume you have the geo coordinates of the bottom left corner and the bottom right corner. You already have the information to convert a geo coordinate into a screen coordinate so the image can be drawn matching up the bottom left corner of your image with the pixel coordinate which you've calculated. I will call this point your anchor point.
At this stage you probably have an image with one corner at the correct location but now it needs to be scaled down or up and then rotated about your anchor point. You can get the current zoom level from your mapView or you can get the latitudeSpan and you can calculate the scale factor to be applied to your image.
Lastly, if you have the geo coordinates of the two corners of the image, you can calculate the angle the image should be rotated. This can be calculated using pythagoras or you can convert from Cartesian coordinates to polar coordinates see here. This calculation doesn't have to be done by your app - it can be calculated separately and put in as a constant. Now you can apply the rotation transform around your fixed anchor point.
You may also want to make use of handy built-in functions such as mapController.zoomInFixing() which takes pixel coordinates or one of the other zoomTo() or animateTo() functions.
Edit: If you're not using a mapview to manage your geo-coordinates then you can apply the image transformations using code like this:
// create a matrix for the manipulation
Matrix matrix = new Matrix();
// resize the bit map
matrix.postScale(scaleWidth, scaleHeight);
// rotate the Bitmap
matrix.postRotate(angle);
// recreate the new Bitmap
Bitmap resizedBitmap = Bitmap.createBitmap(bitmapOrg, 0, 0,
width, height, matrix, true);
// make a Drawable from Bitmap to allow to set the BitMap
// to the ImageView, ImageButton or what ever
BitmapDrawable bmd = new BitmapDrawable(resizedBitmap);
ImageView imageView = new ImageView(this);
// set the Drawable on the ImageView
imageView.setImageDrawable(bmd);
Edit: With the upper left and lower right coordinates, you can calculate the angle as follows:
angle = sin-1((right.x - left.y)/sqrt((right.x - left.x)sq + (right.y - left.y)sq))
[Where sqrt = square root; sq = squared
If you know what is the angle, it is a simple Cartesian rotation of the axis.
Let x be the old longitude,
y be the old latitude,
and b be the angle
The new longitude x' = x*cos(b) - y*sin(b)
The new latitude y' = x*sin(b) + y*cos(b)
I'm not sure I understand but it seems to me like you want to calculate the angle of the image that you want using two points then rotate it and resize it based on the number of pixels between point a and b (two corners) using the method of changing from lat lon to pixels and the distance formula

Categories

Resources