In my Android application I want to request data for the location where the user touches the map. GoogleMap.OnMapClickListener provides the touched position as latitude and longitude coordinates.
public abstract void onMapClick(LatLng point)
In order to pass an area instead of a point I need to calculate the coordinates for a bounding box centered at the touch position. The extend of the bounding box should not depend on the zoom level of the map.
I do not want to request the bounding box of the visible screen - just a small bounding box area around the touch position.
Is there any framework method I could use? Otherwise, how do I find a suitable distance value for the extend of the bounding box?
Isn't LatLngBounds what you need?
You can override onMarkerClick like below
#Override
public boolean onMarkerClick(Marker marker) {
if (markerClicked) {
if (polygon != null) {
polygon.remove();
polygon = null;
}
polygonOptions.add(marker.getPosition());
polygonOptions.strokeColor(ContextCompat.getColor(getContext(), R.color.colorRedTransparent));
polygonOptions.fillColor(ContextCompat.getColor(getContext(), R.color.colorBlueTransparent));
polygonOptions.strokeWidth(5.0f);
polygon = mMap.addPolygon(polygonOptions);
polygon.setClickable(true);
} else {
if (polygon != null) {
polygon.remove();
polygon = null;
}
polygonOptions = new PolygonOptions().add(marker.getPosition());
markerClicked = true;
}
return true;
}
Pass your polygon to generate bounding box
public static Rectangle getBoundingBox(Polygon polygon) {
double boundsMinX = Double.MAX_VALUE; // bottom south latitude of the bounding box.
double boundsMaxX = Double.MIN_VALUE; // top north latitude of bounding box.
double boundsMinY = Double.MAX_VALUE; // left longitude of bounding box (western bound).
double boundsMaxY = Double.MIN_VALUE; // right longitude of bounding box (eastern bound).
for (int i = 0; i < polygon.getPoints().size(); i++) {
double x = polygon.getPoints().get(i).latitude;
boundsMinX = Math.min(boundsMinX, x);
boundsMaxX = Math.max(boundsMaxX, x);
double y = polygon.getPoints().get(i).longitude;
boundsMinY = Math.min(boundsMinY, y);
boundsMaxY = Math.max(boundsMaxY, y);
}
//Rectangle(double left, double bottom, double right, double top)
return new Rectangle(boundsMinY, boundsMinX, boundsMaxY, boundsMaxX);
}
I'm not sure I fully understand your question, but usually screen-to-map coordinates are done using a few methods, probably something like that:
Projection projection = googleMaps.getProjection();
Point p = projection.toScreenLocation(point);
LatLng topLeft = projection.fromScreenLocation(new Point(p.x - halfWidth, p.y - halfHeight));
LatLng bottomRight = projection.fromScreenLocation(new Point(p.x + halfWidth, p.y + halfHeight));
Edit:
on the above
point is the LatLng you received on your onMapClick.
p is the on screen position of said click event
p.x - halfWidth, p.y - halfHeight, p.x + halfWidth, p.y + halfHeight is a bounding box around the click location with 2 * halfWidth of width and 2 * halfHeight of height.
topLeft and bottomRight is a bounding box around the click event in Latitude-Longitude coordinates.
Related
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
Alright so after reading Antonio's comment, I am with this in my code. Now regardless of what I submit as my percentage it still thinks my object is outside the bounding box.
My Position is the marker passed in.
LatLngBounds bounds = mMap.getProjection().getVisibleRegion().latLngBounds;
LatLngBounds newBounds = reduceBy(bounds, 0.05d);
if(newBounds.contains(myPosition.getPosition())) {
//If the item is within the the bounds of the screen
} else{
//If the marker is off screen
zoomLevel -= 1;}
}
return zoomLevel;
}
public LatLngBounds reduceBy(LatLngBounds bounds, double percentage) {
double distance = SphericalUtil.computeDistanceBetween(bounds.northeast, bounds.southwest);
double reduced = distance * percentage;
double headingNESW = SphericalUtil.computeHeading(bounds.northeast, bounds.southwest);
LatLng newNE = SphericalUtil.computeOffset(bounds.northeast, reduced/2d, headingNESW);
double headingSWNE = SphericalUtil.computeHeading(bounds.southwest, bounds.northeast);
LatLng newSW = SphericalUtil.computeOffset(bounds.southwest, reduced/2d, headingSWNE);
return LatLngBounds.builder().include(newNE).include(newSW).build();
}
}
I have all the zoom levels set but sometimes I run into spots such as this where it is still in bounds except the marker is off screen. I want to have a slightly smaller bounding box to detect this and then zoom out one level on only these situations.
You can use SphericalUtil class from the Google Maps API Utility Library to make the calculations:
public LatLngBounds reduceBy(LatLngBounds bounds, double percentage) {
double distance = SphericalUtil.computeDistanceBetween(bounds.northeast, bounds.southwest);
double reduced = distance * percentage;
double headingNESW = SphericalUtil.computeHeading(bounds.northeast, bounds.southwest);
LatLng newNE = SphericalUtil.computeOffset(bounds.northeast, reduced/2d, headingNESW);
double headingSWNE = SphericalUtil.computeHeading(bounds.southwest, bounds.northeast);
LatLng newSW = SphericalUtil.computeOffset(bounds.southwest, reduced/2d, headingSWNE);
return LatLngBounds.builder().include(newNE).include(newSW).build();
}
To reduce your bounds by a 5% (diagonal) you can do:
LatLngBounds newBounds = reduceBy(bounds, 0.05d);
Depending on your requirements for precision, you might want to just use simple interpolation like here:
public LatLngBounds reduceBounds(LatLngBounds bounds, double percentage) {
double north = bounds.northeast.latitude;
double south = bounds.southwest.latitude;
double east = bounds.northeast.longitude;
double west = bounds.southwest.longitude;
double lowerFactor = percentage / 2 / 100;
double upperFactor = (100 - percentage / 2) / 100;
return new LatLngBounds(new LatLng(south + (north - south) * lowerFactor, west + (east - west) * lowerFactor),
new LatLng(south + (north - south) * upperFactor, west + (east - west) * upperFactor));
}
This is very simple Math using +-*/ and doesn't cost a lot of performance.
To reduce your bounds dimensions by 10% you do:
LatLngBounds newBounds = reduceBounds(bounds, 10);
Add error checking and border case handling as needed
I need to rotate my map around current location marker. Current location marker will be placed at the boottom of map. Like on a picture. Map with location marker
I solved my task with this code:
//rotate map
map.setMapOrientation(angle);
//center to current location
GeoPoint point = mMyLocationNewOverlay.getMyLocation();
if (point == null) return;
mapController.setCenter(point);
//calculate translation
float bottomPadding = map.getHeight() * 0.1f;
float radius = map.getHeight()/2 - bottomPadding;
double deltaW = Math.sin(Math.toRadians(angle));
double deltaH = Math.cos(Math.toRadians(angle));
int width = map.getWidth()/2 - (int)(radius * deltaW);
int height = map.getHeight()/2 - (int)(radius * deltaH);
//Move current location marker to bottom of the map
Projection projection = map.getProjection();
GeoPoint centerPoint = (GeoPoint) projection.fromPixels(width, height);
mapController.setCenter(centerPoint);
Is there any possible way of finding radius of the visible map from the middle point?
I want to get the near places against the center point of the map from an API, and that API require lat,lng and radius. I am able to get lat and lng from center point but couldnt find a way to get radius .
thanks
For the Google Maps Android API, you can get the bounds by...
From the map reference, get the Projection by getProjection(). And,
a projection is used to translate between on screen location and geographic coordinates..
So from the projection, we can use the getVisibleRegion(), and to get the VisibleRegion of the map, which contains a LatLngBounds, which is a class that contains 2 LatLng variables, one for the Northeast corner of the bound and one for the Southwest corner.
So the code should look something like this:
googleMap.setOnCameraChangeListener(new GoogleMap.OnCameraChangeListener() {
#Override
public void onCameraChange(CameraPosition position) {
LatLngBounds bounds = googleMap.getProjection().getVisibleRegion().latLngBounds;
LatLng northeast = bounds.northeast;
LatLng southwest = bounds.southwest;
Context context = getApplicationContext();
CharSequence text = "ne:"+northeast+" sw:"+southwest;
int duration = Toast.LENGTH_SHORT;
Toast toast = Toast.makeText(context, text, duration);
toast.show();
}
});
=-=-=-=-=-=
edit:
May be I was too naive, given only the NE and SW can solve this problem, but only under the special case where user did not rotate the map or tilt up for the 3D map.
So instead, you can just grab the VisibleRegion, which provided 4 variable, farRight, farLeft, nearRight, nearLeft, each represent 4 conners of the area.
Then we can calculate the width and height of the area for that 4 points and pick the smaller one (well, sometime width can be greater than height I guess.)
And for the calculation, we can just use the Location.distanceBetween(x1,y1,x2,y2,result) function...
which makes the code look like the following:
VisibleRegion visibleRegion = googleMap.getProjection().getVisibleRegion();
LatLng farRight = visibleRegion.farRight;
LatLng farLeft = visibleRegion.farLeft;
LatLng nearRight = visibleRegion.nearRight;
LatLng nearLeft = visibleRegion.nearLeft;
float[] distanceWidth = new float[2];
Location.distanceBetween(
(farRight.latitude+nearRight.latitude)/2,
(farRight.longitude+nearRight.longitude)/2,
(farLeft.latitude+nearLeft.latitude)/2,
(farLeft.longitude+nearLeft.longitude)/2,
distanceWidth
);
float[] distanceHeight = new float[2];
Location.distanceBetween(
(farRight.latitude+nearRight.latitude)/2,
(farRight.longitude+nearRight.longitude)/2,
(farLeft.latitude+nearLeft.latitude)/2,
(farLeft.longitude+nearLeft.longitude)/2,
distanceHeight
);
float distance;
if (distanceWidth[0]>distanceHeight[0]){
distance = distanceWidth[0];
} else {
distance = distanceHeight[0];
}
thank you so much for your answer #kaho, it helped me alot (even you calculated the distanceWidth and distanceHeight in the same way).
Clarification:
farLeft LatLng object that defines the top left corner of the camera.
farRight LatLng object that defines the top right corner of the camera.
nearLeft LatLng object that defines the bottom left corner of the camera.
nearRight LatLng object that defines the bottom right corner of the camera.
EDITED: I don't know why we made a simple calculation become a bit complicated, the visible radius is just A HALF OF VISIBLE DIAGONAL LINE, that's all!
private double getMapVisibleRadius() {
VisibleRegion visibleRegion = map.getProjection().getVisibleRegion();
float[] diagonalDistance = new float[1];
LatLng farLeft = visibleRegion.farLeft;
LatLng nearRight = visibleRegion.nearRight;
Location.distanceBetween(
farLeft.latitude,
farLeft.longitude,
nearRight.latitude,
nearRight.longitude,
diagonalDistance
);
return diagonalDistance[0] / 2;
}
I also logged my results to compare with #jossef-harush 's results and it's approximately:
Full area, even corners!
I don't see other answers cover the entire map area;
see image below, to test it I drew a circle overlay to see the bounds of the calculated radius, it does not cover entire map area.
my modification is quite simple, I've used Pythagorean theorem to find the suitable radius to contain the map "rectangle".
private double getMapVisibleRadius() {
VisibleRegion visibleRegion = googleMap.getProjection().getVisibleRegion();
float[] distanceWidth = new float[1];
float[] distanceHeight = new float[1];
LatLng farRight = visibleRegion.farRight;
LatLng farLeft = visibleRegion.farLeft;
LatLng nearRight = visibleRegion.nearRight;
LatLng nearLeft = visibleRegion.nearLeft;
Location.distanceBetween(
(farLeft.latitude + nearLeft.latitude) / 2,
farLeft.longitude,
(farRight.latitude + nearRight.latitude) / 2,
farRight.longitude,
distanceWidth
);
Location.distanceBetween(
farRight.latitude,
(farRight.longitude + farLeft.longitude) / 2,
nearRight.latitude,
(nearRight.longitude + nearLeft.longitude) / 2,
distanceHeight
);
double radiusInMeters = Math.sqrt(Math.pow(distanceWidth[0], 2) + Math.pow(distanceHeight[0], 2)) / 2;
return radiusInMeters;
}
For the Kotlin users, call this function from setOnCameraIdleListener
private fun getMapVisibleRadius(): Double {
val visibleRegion: VisibleRegion = mMap.projection.visibleRegion
val distanceWidth = FloatArray(1)
val distanceHeight = FloatArray(1)
val farRight: LatLng = visibleRegion.farRight
val farLeft: LatLng = visibleRegion.farLeft
val nearRight: LatLng = visibleRegion.nearRight
val nearLeft: LatLng = visibleRegion.nearLeft
Location.distanceBetween((farLeft.latitude + nearLeft.latitude) / 2, farLeft.longitude, (farRight.latitude + nearRight.latitude) / 2, farRight.longitude, distanceWidth)
Location.distanceBetween(farRight.latitude,
(farRight.longitude + farLeft.longitude) / 2, nearRight.latitude, (nearRight.longitude + nearLeft.longitude) / 2, distanceHeight)
val radiusInMeters = Math.sqrt((Math.pow(distanceWidth.get(0).toString().toDouble(), 2.0))
+ Math.pow(distanceHeight.get(0).toString().toDouble(), 2.0)) / 2
return radiusInMeters
}
edit: The following answer is for Google Maps JavaScript API v3
=-=-=-=-=-=-=
I think the answer would be: Yes, you can.
According to the documentation, you can calculate distance between 2 points by: computeDistanceBetween(LatLngFrom, LatLngTo)
Also you can get the boundary of the map by using getBounds() method, which is in the google.maps.Map class.
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.