I am trying to center a Google Map to the user location while giving a reasonable zoom level taking into account the accuracy of that location. Could anybody describe how should I compute it? Which variables are involved, how do you achieve this?
What you are looking for is the formula that calculates the zoom level based on the accuracy of the location.
I managed to come up with this formula which (in my tests) worked pretty well.
This can be simplified (might not seem so) to this:
This scary looking thing is what you want.
EquatorLength is 40,075,004 meters. While the Meters/Pixel can be calculated by diving the diameter of the accuracy circle by the length of the device screen (in pixels).
Here's a sample program that I used to test this formula:
GoogleMap mMap;
#Override
protected void onStart() {
super.onStart();
mMap = ((MapFragment)getFragmentManager().findFragmentById(R.id.map)).getMap();
// Enable user's location layer
mMap.setMyLocationEnabled(true);
mMap.setOnMyLocationChangeListener(new GoogleMap.OnMyLocationChangeListener() {
#Override
public void onMyLocationChange(Location location) {
// Location lat-lng
LatLng loc = new LatLng(location.getLatitude(), location.getLongitude());
// Location accuracy diameter (in meters)
float accuracy = location.getAccuracy() * 2;
// Screen measurements
DisplayMetrics metrics = new DisplayMetrics();
getWindowManager().getDefaultDisplay().getMetrics(metrics);
// Use min(width, height) (to properly fit the screen
int screenSize = Math.min(metrics.widthPixels, metrics.heightPixels);
// Equators length
long equator = 40075004;
// The meters per pixel required to show the whole area the user might be located in
double requiredMpp = accuracy/screenSize;
// Calculate the zoom level
double zoomLevel = ((Math.log(equator / (256 * requiredMpp))) / Math.log(2)) + 1;
Log.e(TAG, String.format("Accuracy: %f. Screen Width: %d, Height: %d",
accuracy, metrics.widthPixels, metrics.heightPixels));
Log.e(TAG, String.format("Required M/Px: %f Zoom Level: %f Approx Zoom Level: %d",
requiredMpp, zoomLevel, calculateZoomLevel(screenSize, accuracy)));
// Center to user's position
mMap.animateCamera(CameraUpdateFactory.newLatLngZoom(loc, (float) zoomLevel));
// Prevent the camera centering on the user again
mMap.setOnMyLocationChangeListener(null);
}
});
}
private int calculateZoomLevel(int screenWidth, float accuracy) {
double equatorLength = 40075004; // in meters
double metersPerPixel = equatorLength / 256;
int zoomLevel = 1;
while ((metersPerPixel * (double) screenWidth) > accuracy) {
metersPerPixel /= 2;
zoomLevel++;
}
return zoomLevel;
}
Few things to note:
This answer is based on this and implements it to check the values generated
Accuracy is the radius of user's location and according to the docs it can be up to 68% correct.
Any corrections are very welcome.
If you're looking for something simple:
var zoom = Math.min(20, Math.max(1, Math.log2(591657550/accuracy)-2));
Tweak -2 to get the desired zoom.
Checkout this answer for a chart corresponding zoom with accuracy.
Thanks #Simas! I picked up your algo to make this extension to GMSMapView to calculate for the ideal zoomLevel given the accuracy of a CLLocation.
I had to make adjustments to consider devices with Retina displays since each pixel is not exactly the same as 1 point on the screen:
extension GMSMapView {
func getIdealZoomLevel(usingLocation location:CLLocation)->Float {
let retinaScale = Double(UIScreen.mainScreen().scale)
let equatorLength : Double = 40075004 // in meters
var metersPerPixel = equatorLength / 256
let accuracy = location.horizontalAccuracy
// I used height because I'm on landscape, but moving forward I'll have to get the Min of the width and height.
// I also took only 75% of the height to give it some margin
let screenWidth : Double = Double( self.frame.size.height) * 0.75
var display = metersPerPixel * (screenWidth / retinaScale)
var zoomLevel : Float = 0.0
while (display > accuracy) {
metersPerPixel /= 2
display = metersPerPixel * (screenWidth / retinaScale)
zoomLevel += 1
}
return zoomLevel
}
}
It's for a Swift project I'm working on and right now, I'm able to display a good proximity enclosing the radius within the given CLLocation.
Hope this helps.
Related
I got zoom level of 3-21. zoom out-zoom in respectively.
But i want to compute that zoom level to meter/km distance with the given bar(screenwidth?) of the screen like Gmaps scale bar do.
Example :
zoom level 20 = hundreds of meter
zoom levevl 3 = thousands of km or a mile
TIA
You can find the radius between right/left most point and center of the map when map becomes idle by using
/**
* Method used to fetch Map radius
*/
private int getMapRadius() {
LatLng latlng = googleMap.getProjection().getVisibleRegion().latLngBounds.getCenter();
if (latLng == null)
return 0;
LatLng latLng1 = googleMap.getProjection().getVisibleRegion().farRight;
return (int) MapUtils.computeDistance(latLng1.latitude, latLng1.longitude, latLng.latitude, latLng.longitude);
}
What i am trying to achieve is to draw dot at the exact same location as black dot already present in image , but there are problem that i have to face regarding image:
Image can change its densities in multiple screens(so no fixed size)
Given image can be of any size , i have to convert it to appropriate size to show on custom View.
and How to show image in custom view so its density remain the same ?
Now problems regarding dots drawing:
How can i identify black dots x,y axis dynamically to draw at the same exact black dot drawn in image of custom view.
Is there a good way to achieve question 1 dynamically or i should hard code dots.
What i have done so far:
I have moved all images to drawable folder and done layout_width = wrap_content and height the same as width.
<com.jutt.dotbot.ConnectDotsView
android:id="#+id/gfxImage"
android:layout_width="500dpi"
android:layout_height="600dpi"
android:layout_marginTop="100dip"
android:layout_centerHorizontal="true"/>
I've look hard coded points by Toast the point on each ACTION_DOWN in onTouchEvent and then noted then and given to the class ConnectDotsView .
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
touch_start(x, y);
invalidate();
L.s(getContext(), "x = "+x+"y = "+y);
break;
After i've noted all points drawn , i am giving this class back that same points to draw , by calling:
public void setPoints(List<Point> points) {
this.mPoints = points;
}
Now Biggest problem [which i think still persist] , (x,y) on screen 1152w*720h may not be same on 480w*600h , so what i've done is who a function to actually convert those points.
private static final int OLDSCREENX = 1152;
private static final int OLDSCREENY = 720;
private int[] makeDimentionGeneric(float oldScreenX, float oldScreenY,
float oldX, float oldY) {
// get screen size
DisplayMetrics displaymetrics = new DisplayMetrics();
getWindowManager().getDefaultDisplay().getMetrics(displaymetrics);
final float y = displaymetrics.heightPixels;
final float x = displaymetrics.widthPixels;
int[] i = new int[] { (int) ((oldX / oldScreenX) * x),
(int) ((oldY / oldScreenY) * y) };
return i;
}
and i am calling this while setting the points like makeDimentionGeneric(OLDSCREENX, OLDSCREENY, p.x, p.y); when p.x and p.y are hardcoded stored value from screen.
Now what happens when i try to run it in different devices.
When am i doing it wrong and how can i achieve it correctly ? please guys i've worked hard studied but can't seems to find a better approach than i've mentioned.
[Update] as requested(21 jul 2015):
private int[] makeDimentionGeneric(float oldScreenX, float oldScreenY,
float oldX, float oldY) {
// convert all px to dp
oldScreenY = convertPixelsToDp(oldScreenY, this);
oldScreenX = convertPixelsToDp(oldScreenX,this);
oldX = convertPixelsToDp(oldX,this);
oldY = convertPixelsToDp(oldY,this);
// get screen size
DisplayMetrics displaymetrics = new DisplayMetrics();
getWindowManager().getDefaultDisplay().getMetrics(displaymetrics);
final float y = convertPixelsToDp(displaymetrics.heightPixels, this);
final float x = convertPixelsToDp(displaymetrics.widthPixels, this);
int[] i = new int[] { (int)((oldX / oldScreenX) * x),
(int) ((oldY / oldScreenY) * y) };
return i;
}
Since your image differs depending on DPI your pixel calculations must also differ depending on DPI. Consider the following question:
Converting pixels to dp
In your case (copied from answer in above question) you probably want to use this method:
/**
* This method converts device specific pixels to density independent pixels.
*
* #param px A value in px (pixels) unit. Which we need to convert into db
* #param context Context to get resources and device specific display metrics
* #return A float value to represent dp equivalent to px value
*/
public static float convertPixelsToDp(float px, Context context){
Resources resources = context.getResources();
DisplayMetrics metrics = resources.getDisplayMetrics();
float dp = px / (metrics.densityDpi / 160f);
return dp;
}
I created my first JavaFX app that displays images. I want it to zoom the image to full size on mouse down (centered on cursor position) and to refit the image on mouse up.
All is working fine but the i don't know how to center on cursor position. My zoom method looks like that at the moment:
private void zoom100(double cursorx, double cursory){
double centery = imageView.getLayoutBounds().getHeight()/2;
double centerx = imageView.getLayoutBounds().getWidth()/2;
imageView.setFitHeight(-1); //zooms height to 100%
imageView.setFitWidth(-1); //zooms width to 100%
imageView.setTranslateX(centerx-cursorx); //moves x
imageView.setTranslateY(centery-cursory); //moves y
}
My idea is to set an offset with translate. I am not sure if translate is the correct approach. If it is the correct approach how to calculate the correct values? (centerx-cursorx) is wrong!
I uploaded the code here: https://github.com/dermoritz/FastImageViewer (it is working now - see my answer)
I didn't test it but I think the order is wrong. you calculate the center before you set the fit size. By setting the fit size your image view will change it's preferred size. therefore the center point won't match anymore.
I found a solution, but i am still not sure if this is a good way to go:
private void zoom100(double x, double y) {
double oldHeight = imageView.getBoundsInLocal().getHeight();
double oldWidth = imageView.getBoundsInLocal().getWidth();
boolean heightLarger = oldHeight>oldWidth;
imageView.setFitHeight(-1);
imageView.setFitWidth(-1);
//calculate scale factor
double scale=1;
if(heightLarger){
scale = imageView.getBoundsInLocal().getHeight()/oldHeight;
}else {
scale = imageView.getBoundsInLocal().getWidth()/oldWidth;
}
double centery = root.getLayoutBounds().getHeight() / 2;
double centerx = root.getLayoutBounds().getWidth() / 2;
double xOffset = scale * (centerx - x);
double yOffset = scale *(centery - y);
imageView.setTranslateX(xOffset);
imageView.setTranslateY(yOffset);
}
The picture must be centered within "root". I think there should be something more direct - only using properties of imageview?!
Meanwhile the app works using this code: https://github.com/dermoritz/FastImageViewer
I am trying to calculate the height above map (ignoring topography) given a zoom level. I know the equation for scale at a specific zoom level is 591657550.5/2^(level-1) (https://gis.stackexchange.com/questions/7430/google-maps-zoom-level-ratio), but I am unsure on how to use this information (or whether or not this is the right information) to solve for height above map. Any help is appreciated.
I set my google map size to 5cm selected a zoom level, and then re-found that location with that zoom in google earth to get a eye altitude level (the D value in the angular size equation http://en.wikipedia.org/wiki/Forced_perspective). I was able to find the h value in the angular size equation by first setting my map length on screen to 5cm, and then using the scale equation of 591657550.5/2^(level-1) *5cm to calculate the h value in the angular size equation. Knowing these two variables I was able to calculate the constant angle for which google maps displayed images when maps was at a 5cm width (85.36222058). From these pieces of information I was able to construct this method which calculates eye altitude above map from zoom level with relative accuracy
public float getAltitude(float mapzoom){
//this equation is a transformation of the angular size equation solving for D. See: http://en.wikipedia.org/wiki/Forced_perspective
float googleearthaltitude;
float firstPartOfEq= (float)(.05 * ((591657550.5/(Math.pow(2,(mapzoom-1))))/2));//amount displayed is .05 meters and map scale =591657550.5/(Math.pow(2,(mapzoom-1))))
//this bit ^ essentially gets the h value in the angular size eq then divides it by 2
googleearthaltitude =(firstPartOfEq) * ((float) (Math.cos(Math.toRadians(85.362/2)))/(float) (Math.sin(Math.toRadians(85.362/2))));//85.362 is angle which google maps displays on a 5cm wide screen
return googleearthaltitude;
}
Sorry if my explanation is poorly explained. If you guys want to use this method feel free to. Sorry for any poorly worded sentences.
I have basically converted Javascript code to Java. I hope this works.
public int convertRangeToZoom(double range) {
//see: google.maps.v3.all.debug.js
int zoom = (int) Math.round(Math.log(35200000 / range) / Math.log(2));
if (zoom < 0) zoom = 0;
else if (zoom > 19) zoom = 19;
return zoom;
}
public int convertZoomToRange(double zoom){
//see: google.maps.v3.all.debug.js
int range = (int) 35200000/(Math.pow(2, zoom));
if (range < 300) range = 300;
return range;
}
https://groups.google.com/forum/#!msg/google-earth-browser-plugin/eSL9GlAkWBk/T4mdToJz_FgJ
I am trying to setup a layer using worldwind java and i want to render icons on the map at their specific geo locations. I have that working but i want to be able to zoom to where all the icons are. Is there an easy way to do that? Im not really sure where to start.. Are there existing methods for zooming in on a group of points?
First you need to calculate the Sector containing all of your points. e.g.
Sector boundingSector = Sector.boundingSector(points);
//public static Sector boundingSector(Iterable<? extends LatLon> itrbl)
Now here's some code taken from ScankortDenmark example to calculate the zoom you need to fit the whole sector on screen:
// From ScankortDenmark example
public static double computeZoomForExtent(Sector sector)
{
Angle delta = sector.getDeltaLat();
if (sector.getDeltaLon().compareTo(delta) > 0)
delta = sector.getDeltaLon();
double arcLength = delta.radians * Earth.WGS84_EQUATORIAL_RADIUS;
double fieldOfView = Configuration.getDoubleValue(AVKey.FOV, 45.0);
return arcLength / (2 * Math.tan(fieldOfView / 2.0));
}
182Much's answer does work under under some conditions. However, a better solution must take into account that the Horizontal FOV (Field of View) is not always fixed at 45.0 degrees. It also needs to take into account the Vertical FOV. Even how the positions end of clustering has to be taken into account. Meaning, do the positions spread out more East to West or North and South. Is the users view of the globe (WorldWindow) actually skinnier then the height. All of these factors come into account when calculating the needed zoom level to view all positions. I created this static method to account for all of the listed positions above. As a side note, you can have slightly better precision if you calculate the actual mean radius of the Earth for where your positions tend to cluster instead of taken Earth.WGS84_EQUATORIAL_RADIUS. But this is almost negligible so I leave that part out here.
/**
* Calculates the altitude in meters needed to view all of the given points.
* This method is safe for any window sizing configurations. If the
* WorldWindor arg is null then a static max altitude value of 1,0667,999
* meters is returned. if the WorldWindow is good but the list of Positions
* is null or empty then the current zoom level of the WorldWindow is
* returned. If the list of positions cannot all be seen on the globe
* because some positions are on the other side of the globe then a static
* max altitude value of 1,0667,999 meters is returned.
*
* #param positions
* - a list of positions wanted to view
* #return the altitude in meters needed to view all of the given points.
*/
public static double getZoomAltitude(List<Position> positions, WorldWindow wwd) {
double zoom = 10667999;
if (wwd != null) {
// Gets the current zoom as a fail safe to return
BasicOrbitView orbitView = (BasicOrbitView) wwd.getView();
zoom = orbitView.getZoom();
// zoom is in meters and and is limited the max zoom out to 10,667,999 meters
int MAX_ZOOM = 10667999;
if (positions != null && !positions.isEmpty()) {
Sector sector = Sector.boundingSector(positions);
if (sector != null) {
// This calculation takes into account the window sizing configuration of the map in order to accurately
// display the list of positions.
double meanRadius = Earth.WGS84_EQUATORIAL_RADIUS;
// Next we must calculate the zoom levels for both delta latitude viewing and delta longitude viewing.
// generally, a group of positions that spread out more Longitudenal viewing (wider viewing width)
// holds a constant 45.0 degree field of view (FOV). The horizontal FOV can be changed so this input
// must handle dynamically as well. The latitudenal (positon group runs more East to West then North and South)
// position group have a dynamic FOV that changes depending on the users sizing of the map. These have
// to be handled any time the group of positions has a greater delta latitude than delta longitude.
// Also if the user has a skinny map this will effect the output calculation and must be handled.
// Here we take all the dynamic variables into account for both types of possibilities and choose
// the larger zoom level of them.
int deltaLon = new BigDecimal(sector.getDeltaLon().radians * meanRadius).intValue();
int deltaLat = new BigDecimal(sector.getDeltaLat().radians * meanRadius).intValue();
System.out.println("deltaLonAL Wider: " + deltaLon + "\tdeltaLatAL Taller: " + deltaLat);
double horizontalFOV = orbitView.getFieldOfView().getDegrees();
double verticalFOV = ViewUtil.computeVerticalFieldOfView(orbitView.getFieldOfView(),
orbitView.getViewport()).getDegrees();
double lonZoomLevel = new BigDecimal((deltaLon / 2.0) / (Math.tan(horizontalFOV / 2.0))).intValue();
double latZoomLevel = new BigDecimal((deltaLat / 2.0)
/ (Math.tan(Math.toRadians(verticalFOV) / 2.0))).intValue();
System.out
.println("LonZoomLevel Wider: " + lonZoomLevel + "\tLatZoomLevel Taller: " + latZoomLevel);
double zoomLevel = Math.max(lonZoomLevel, latZoomLevel);
System.out.println("zoomLevel meters: " + zoomLevel + "\tfeet: "
+ new BigDecimal(zoomLevel * 3.2808));
// zoom is the altitude measured in meters to view a given area calculated to fit the viewing
// window edge to edge. A buffer is needed around the area for visual appeal. The bufferedZoom
// is a calculated linear equation (y = 1.0338x + 96177 where R² = 1) It gives the same buffer
// boundary around a group of position depending on the calculated zoom altitude.
double bufferedZoom = 1.0338 * zoomLevel + 96177;
zoom = new BigDecimal(bufferedZoom).intValue();
if (zoom > MAX_ZOOM) {
zoom = MAX_ZOOM;
System.out.println("MAX_ZOOM applied");
}
}
} else {
System.out.println("getZoomAltitude method cannot calculate the zoom because the points passed in was null and the current zoom was returned.");
}
}
return zoom;
}