I have a circular overlay that can change according to the user's preferences: they have a circle of radius 'r' around them and a slider can change 'r' accordingly. So far it works perfectly well.
My problem is I don't know how to find the proper conversion from the circle's radius to the map's metric. As an example, given the circle below, what is the distance covered with the given radius?
You must know what is the scale of the map you are showing. Having scale of the map you could convert pixels size to metric size. And having this proportion you can convert radius given in pixels to meters.
So here's how you do it...
Jedil is correct you have to find your screen width/height, the dpi of the screen, etc. So, looking through this old osmdroid source I sorta figured out this:
//Get the x and y dpi
this.xdpi = this.context.getResources().getDisplayMetrics().xdpi;
this.ydpi = this.context.getResources().getDisplayMetrics().ydpi;
//Get the screen width/height
this.screenWidth = this.context.getResources().getDisplayMetrics().widthPixels;
this.screenHeight = this.context.getResources().getDisplayMetrics().heightPixels;
// DPI corrections for specific models
String manufacturer = null;
try {
final Field field = android.os.Build.class.getField("MANUFACTURER");
manufacturer = (String) field.get(null);
} catch (final Exception ignore) {
}
if ("motorola".equals(manufacturer) && "DROIDX".equals(android.os.Build.MODEL)) {
// If the screen is rotated, flip the x and y dpi values
WindowManager windowManager = (WindowManager) this.context
.getSystemService(Context.WINDOW_SERVICE);
if (windowManager.getDefaultDisplay().getOrientation() > 0) {
this.xdpi = (float) (this.screenWidth / 3.75);
this.ydpi = (float) (this.screenHeight / 2.1);
} else {
this.xdpi = (float) (this.screenWidth / 2.1);
this.ydpi = (float) (this.screenHeight / 3.75);
}
} else if ("motorola".equals(manufacturer) && "Droid".equals(android.os.Build.MODEL)) {
// http://www.mail-archive.com/android-developers#googlegroups.com/msg109497.html
this.xdpi = 264;
this.ydpi = 264;
}
// set default max length to 1 inch
maxLength = 2.54f;
That's how you get the 'constants' (at least in the device's eyes). To convert...
// calculate dots per centimeter
int xdpcm = (int) ((float) xdpi / 2.54);
int ydpcm = (int) ((float) ydpi / 2.54);
// get length in pixel
int xLen = (int) (maxLength * xdpcm);
int yLen = (int) (maxLength * ydpcm);
// Two points, xLen apart, at scale bar screen location
IGeoPoint p1 = projection.fromPixels((screenWidth / 2) - (xLen / 2), yOffset);
IGeoPoint p2 = projection.fromPixels((screenWidth / 2) + (xLen / 2), yOffset);
// get distance in meters between points
final int xMeters = ((GeoPoint) p1).distanceTo(p2);
...and it's a similar, if not near-identical, matter to get the yMeters (hint: use screenHeight).
Admittedly, I'm not totally sure of what the lines IGeoPoint... are doing but I realize the conversion is there. Hoping this helps someone out in the future. For a better understanding of the above code, please see the link I've posted.
I think what you're looking for is a meters-to-pixels calculation. You can get this from the projection:
// Get projection
Projection proj = mMapView.getProjection();
// How many pixels in 100 meters for this zoom level
float pixels = proj.metersToPixels(100);
// How many meters in 100 pixels for this zoom level
float meters = 1 / proj.metersToPixels(1 / 100);
// You could also get a raw meters-per-pixels value by using TileSystem.GroundResolution()
Two things to remember - this value will change not only based on what zoom level but based on what latitude you are at on the maps.
Related
I have a Page which contains an AbsoluteLayout with a PinchZoomContainer (like the one from microsoft docs). In this Container is an image that fills the entire screen. When the user tapes on the image, another image (a pin) is positioned at the tappostion. So far this works. My problem is, that i couldn't figure out, how to calculate the position of the added image (user tapped) when the user zooms in and out.
I want the added image to stay at the tapped position, no matter if the user zooms in or out.
When i put the image in a grid the calculation gets done automatically.
Is there a better way to achieve that? I've chosen AbsoluteLayout because i need to put the image at the tapped position.
The following code is used for zooming. Here I can't figure out how to do the calculation for the added image. The image gets added to the AbsoluteLayout on runtime.
Normal scale
Zoomed in
void OnPinchUpdated(object sender, PinchGestureUpdatedEventArgs e)
{
if (e.Status == GestureStatus.Started)
{
// Store the current scale factor applied to the wrapped user interface element,
// and zero the components for the center point of the translate transform.
startScale = Content.Scale;
Content.AnchorX = 0;
Content.AnchorY = 0;
startTransX = pin.TranslationX;
startTranxY = pin.TranslationY;
}
if (e.Status == GestureStatus.Running)
{
// Calculate the scale factor to be applied.
currentScale = currentScale + (e.Scale - 1) * startScale;
currentScale = Math.Max(1, currentScale);
// The ScaleOrigin is in relative coordinates to the wrapped user interface element,
// so get the X pixel coordinate.
double renderedX = Content.X + xOffset;
double deltaX = renderedX / Width;
double deltaWidth = Width / (Content.Width * startScale);
double originX = (e.ScaleOrigin.X - deltaX) * deltaWidth;
// The ScaleOrigin is in relative coordinates to the wrapped user interface element,
// so get the Y pixel coordinate.
double renderedY = Content.Y + yOffset;
double deltaY = renderedY / Height;
double deltaHeight = Height / (Content.Height * startScale);
double originY = (e.ScaleOrigin.Y - deltaY) * deltaHeight;
// Calculate the transformed element pixel coordinates.
double targetX = xOffset - (originX * Content.Width) * (currentScale - startScale);
double targetY = yOffset - (originY * Content.Height) * (currentScale - startScale);
// Apply translation based on the change in origin.
// needs to use Xamarin.Forms.Internals or Extension
Content.TranslationX = targetX.Clamp(-Content.Width * (currentScale - 1), 0);
Content.TranslationY = targetY.Clamp(-Content.Width * (currentScale - 1), 0);
x = Content.TranslationX;
y = Content.TranslationY;
// Apply scale factor
Content.Scale = currentScale;
}
if (e.Status == GestureStatus.Completed)
{
// Store the translation delta's of the wrapped user interface element.
xOffset = Content.TranslationX;
yOffset = Content.TranslationY;
x = Content.TranslationX;
y = Content.TranslationY;
}
}
I read here about converting dp units to pixel units. But I cant understand the 0.5f. Where does this number come from and what is the use of it?
// The gesture threshold expressed in dp
private static final float GESTURE_THRESHOLD_DP = 16.0f;
// Get the screen's density scale
final float scale = getResources().getDisplayMetrics().density;
// Convert the dps to pixels, based on density scale
mGestureThreshold = (int) (GESTURE_THRESHOLD_DP * scale + 0.5f);
// Use mGestureThreshold as a distance in pixels...
Casting floating point numbers to integer will floor them.
That 0.5f is to round the number:
x = (int) 3.9
print x // 3
x = (int) 3.9 + 0.5f
print x // 4
Its to round things. Scale may be a decimal (like 1.5). This means the product may not be a whole number. Adding .5 then converting to int ensures that the number rounds up if the number is more than halfway between two integers, and down if its less than halfway.
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
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
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;
}