Google Maps API Snap Marker To Polyline - google-maps

I have a polyline list (from Google Directions API) that has been drawn on Google Map. Next, I'd like for my marker to move along this line without using Google Maps Roads API.
To tackle this, here's what I'm currently doing:
From the polyline list, I'm selecting two end-points of the path A and B.
Finding out the bearing from A to B.
Creating a list of LatLng points between A and B that are 1 meter apart from each other.
Iterating through the list to check the GPS coordinates for the nearest LatLng.
Setting my marker's location to the nearest LatLng.
The above works in some cases but mostly the marker displays parallel to the polyline not on it.
Is there any other way to make my marker snap to the polyline?

Here is my own implementation for snapping marker to polyline (based on GPS location):
First, use the polyline points to create a list of LatLng that are at a smaller distance (I'm using a one meter distance specified in splitPathIntoPoints function):
for (int i = 0; i < polylinePoints.size(); i++) {
LatLng src = new LatLng(Double.parseDouble(polylinePoints.get(i).get("lat")), Double.parseDouble(polylinePoints.get(i).get("lng")));
if (polylinePoints.size() > i + 1) {
LatLng dest = new LatLng(Double.parseDouble(polylinePoints.get(i + 1).get("lat")), Double.parseDouble(polylinePoints.get(i + 1).get("lng")));
List<LatLng> splitPoints = splitPathIntoPoints(src, dest);
mSplitPoints.addAll(splitPoints);
} else {
break;
}
}
Code for splitPathIntoPoints:
public static List<LatLng> splitPathIntoPoints(LatLng source, LatLng destination) {
Float distance = findDistance(source, destination);
List<LatLng> splitPoints = new ArrayList<>();
splitPoints.add(source);
splitPoints.add(destination);
while (distance > 1) {
int polypathSize = splitPoints.size();
List<LatLng> tempPoints = new ArrayList<>();
tempPoints.addAll(splitPoints);
int injectionIndex = 1;
for (int i = 0; i < (polypathSize - 1); i++) {
LatLng a1 = tempPoints.get(i);
LatLng a2 = tempPoints.get(i + 1);
splitPoints.add(injectionIndex, findMidPoint(a1, a2));
injectionIndex += 2;
}
distance = findDistance(splitPoints.get(0), splitPoints.get(1));
}
return splitPoints;
}
Code for findDistance:
public static Float findDistance(LatLng source, LatLng destination) {
Location srcLoc = new Location("srcLoc");
srcLoc.setLatitude(source.latitude);
srcLoc.setLongitude(source.longitude);
Location destLoc = new Location("destLoc");
destLoc.setLatitude(destination.latitude);
destLoc.setLongitude(destination.longitude);
return srcLoc.distanceTo(destLoc);
}
Code for findMidPoint:
public static LatLng findMidPoint(LatLng source, LatLng destination) {
double x1 = toRad(source.latitude);
double y1 = toRad(source.longitude);
double x2 = toRad(destination.latitude);
double y2 = toRad(destination.longitude);
double Bx = Math.cos(x2) * Math.cos(y2 - y1);
double By = Math.cos(x2) * Math.sin(y2 - y1);
double x3 = toDeg(Math.atan2(Math.sin(x1) + Math.sin(x2), Math.sqrt((Math.cos(x1) + Bx) * (Math.cos(x1) + Bx) + By * By)));
double y3 = y1 + Math.atan2(By, Math.cos(x1) + Bx);
y3 = toDeg((y3 + 540) % 360 - 180);
return new LatLng(x3, y3);
}
Once mSplitPoints is filled with smaller polyline points that are separated by 1 meter apart from each other, the function below finds the snapped location on the polyline based on my current GPS location. Note that mMinorIndexTravelled is a private field in my class with initial value set to zero.
public static LatLng snapToPolyline(LatLng currentLocation) {
LatLng snappedLatLng = null;
Location current = new Location("current");
current.setLatitude(currentLocation.latitude);
current.setLongitude(currentLocation.longitude);
Integer minConfirmCount = 0;
float currentMinDistance = 0, previousMinDistance = 0;
List<Float> distances = new ArrayList<>();
for (LatLng point: mSplitPoints.subList(mMinorIndexTravelled, mSplitPoints.size() - 1)) {
Location pointLoc = new Location("pointLoc");
pointLoc.setLatitude(point.latitude);
pointLoc.setLongitude(point.longitude);
distances.add(current.distanceTo(pointLoc));
previousMinDistance = currentMinDistance;
currentMinDistance = Collections.min(distances);
if (currentMinDistance == previousMinDistance) {
minConfirmCount++;
if (minConfirmCount > 10) {
mMinorIndexTravelled = distances.indexOf(currentMinDistance) + mMinorIndexTravelled;
snappedLatLng = mSplitPoints.get(mMinorIndexTravelled);
break;
}
}
}
return snappedLatLng;
}

Related

How do I get the latitude and longitude of a rectangle if I know the center and top left GPS positions?

Background:
I'm trying to use a Google maps ground overlay but we are missing GPS positions.
We have a map to overlay that was drawn over a Google Maps screenshot. From this screenshot we recorded the Top Left and Center Center position.
We require the bottom right position to accurately overlay these images. See below:
The intial thinking is finding the difference between the two points and adding that onto the center point.
Attempted logic:
In JavaScript:
var topLeft = [-32.8830055, 151.686214];
var centerCenter = [-32.9293803, 151.756686];
var difference1 = centerCenter[0] - ( (topLeft[0] - centerCenter[0] ) ) ;
var difference2 = centerCenter[1] - ( (topLeft[1] - centerCenter[1] ) ) ;
// being bottom right this should be "bigger" than above values
// outputs [-32.97575509999999, 151.827158];
var bottomRight = [difference1 , difference2];
Problem:
However it appears this is where the curve of the earth throws that simple maths out the door. I'm guessing that what is happening below is why.
So given I have a rectangle overlay and I know the top left point and the center point can I work out the latitude and longitude of the bottom right point I.e X in the above diagram. Note I do not know the real world distances of this rectangle.
Note: I also know that I will have to change these to NW / SE to use ground overlays.
Approach(using the geometry-library):
calculate the heading from northwest to center
google.maps.geometry.spherical.computeHeading(northwest, center);
calculate the distance from northwest to center
google.maps.geometry.spherical.computeDistanceBetween(northwest, center);
calculate southeast by using
google.maps.geometry.spherical.computeOffset(center,
calculatedDistance,
calculatedHeading);
function initialize() {
var nw = new google.maps.LatLng(62.400471, -150.287132),
center = new google.maps.LatLng(62.341145, -150.14637),
map = new google.maps.Map(document.getElementById('map_canvas'), {
zoom: 9,
center: center
}),
heading, distance, se;
heading = google.maps.geometry.spherical.computeHeading(nw, center);
distance = google.maps.geometry.spherical.computeDistanceBetween(nw, center);
se = google.maps.geometry.spherical.computeOffset(center, distance, heading);
new google.maps.Marker({
map: map,
position: center
});
new google.maps.Marker({
map: map,
position: nw
});
new google.maps.Marker({
map: map,
position: se
});
new google.maps.GroundOverlay(
'https://developers.google.com/maps/documentation/' +
'javascript/examples/full/images/talkeetna.png', {
north: nw.lat(),
south: se.lat(),
west: nw.lng(),
east: se.lng()
}, {
map: map
});
}
google.maps.event.addDomListener(window, 'load', initialize);
html,
body,
#map_canvas {
height: 100%;
margin: 0;
padding: 0;
}
<div id="map_canvas"></div>
<script src="https://maps.googleapis.com/maps/api/js?v=3&libraries=geometry"></script>
Are you averse to using a library that already has support for Haversine calculation of center points? (With tests to boot)
Check out Geolib: https://github.com/manuelbieh/Geolib
var center = geolib.getCenter([
{latitude: 52.516272, longitude: 13.377722},
{latitude: 51.515, longitude: 7.453619},
{latitude: 51.503333, longitude: -0.119722}
]);
console.log(center);
// Output:
//
// {
// "latitude": "52.009802",
// "longitude": "6.629000",
// "distance": 932.209
// }
A codepen demo you can play with: http://codepen.io/anon/pen/grbGrz/?editors=1010
You can always pare it down to just the function you need.
You'll likely require the Haversine formula for such a calculation. The math involved is obviously at a higher level than is typical for SO answers though so your question might actually belong on gis or math stack exchanges where some more experienced SO users might be able to give you a more detailed answer / example.
lest start from beginning of your query. you have two latlng and now you want to get the latlng of the rectangle.
we will go with the calculation part first and then to the programming part. the assumptions are-
center point = e
top left = A
top right = B
bottom right = C
bottom left = D
mid point of AD=f
mid point of AB=g
Calculation Part
location g parameters- lat-A.lat, long-e.long
location f parameters- lat-e.lat, long-A.long
distance of A to g is = A.distanceTo(g)
distance of A to f is = A.distanceTo(f)
point B = 2Ag from A
point C = 2Af from B
point D = 2Af from A
Programming Part
LatLng A = null, B = null, C = null, D = null, e = null, f = null, g = null,temp=null;
e.latitude = your center latitude value;
e.longitude = your center longitude value;
A.latitude=your top left point latitude value;
A.longitude=your top left point longitude value;
f.latitude = e.latitude;
f.longitude = A.longitude;
g.latitude = A.latitude;
g.longitude = e.longitude;
double[] Ag = new double[1];
double[] Af = new double[1];
Location.distanceBetween(A.latitude, A.longitude, g.latitude, g.longitude, Ag);
Location.distanceBetween(A.latitude, A.longitude, f.latitude, f.longitude, Af);
temp=getDestinationPoint(A,90,(2*Ag));
B.latitude=temp.latitude;
B.longitude=temp.longitude;
temp=getDestinationPoint(B,180,(2*Af));
C.latitude=temp.latitude;
C.longitude=temp.longitude;
temp=getDestinationPoint(A,180,(2*Af));
D.latitude=temp.latitude;
D.longitude=temp.longitude;
private LatLng getDestinationPoint (LatLng source,double brng, double dist){
dist = dist / 6371;
brng = Math.toRadians(brng);
double lat1 = Math.toRadians(source.latitude), lon1 = Math.toRadians(source.longitude);
double lat2 = Math.asin(Math.sin(lat1) * Math.cos(dist) +
Math.cos(lat1) * Math.sin(dist) * Math.cos(brng));
double lon2 = lon1 + Math.atan2(Math.sin(brng) * Math.sin(dist) *
Math.cos(lat1),
Math.cos(dist) - Math.sin(lat1) *
Math.sin(lat2));
if (Double.isNaN(lat2) || Double.isNaN(lon2)) {
return null;
}
return new LatLng(Math.toDegrees(lat2), Math.toDegrees(lon2));
}
Explanation
the point f and g are the mid points of the line AD and AB respectively. we can get it by altering the lat and long value of A and e points. and by these two lengths (Af and Ag) we can get the four latlng points of the rectangle as you require.
thank you
Use the below javascript function for getting lat/long list of the rectangle based on center and distance.latLngArr is a list of rectangle points.
Number.prototype.degreeToRadius = function () {
return this * (Math.PI / 180);
};
Number.prototype.radiusToDegree = function () {
return (180 * this) / Math.PI;
};
function getBoundingBox(fsLatitude, fsLongitude, fiDistanceInKM) {
if (fiDistanceInKM == null || fiDistanceInKM == undefined || fiDistanceInKM == 0)
fiDistanceInKM = 1;
var MIN_LAT, MAX_LAT, MIN_LON, MAX_LON, ldEarthRadius, ldDistanceInRadius, lsLatitudeInDegree, lsLongitudeInDegree,
lsLatitudeInRadius, lsLongitudeInRadius, lsMinLatitude, lsMaxLatitude, lsMinLongitude, lsMaxLongitude, deltaLon;
// coordinate limits
MIN_LAT = (-90).degreeToRadius();
MAX_LAT = (90).degreeToRadius();
MIN_LON = (-180).degreeToRadius();
MAX_LON = (180).degreeToRadius();
// Earth's radius (km)
ldEarthRadius = 6378.1;
// angular distance in radians on a great circle
ldDistanceInRadius = fiDistanceInKM / ldEarthRadius;
// center point coordinates (deg)
lsLatitudeInDegree = fsLatitude;
lsLongitudeInDegree = fsLongitude;
// center point coordinates (rad)
lsLatitudeInRadius = lsLatitudeInDegree.degreeToRadius();
lsLongitudeInRadius = lsLongitudeInDegree.degreeToRadius();
// minimum and maximum latitudes for given distance
lsMinLatitude = lsLatitudeInRadius - ldDistanceInRadius;
lsMaxLatitude = lsLatitudeInRadius + ldDistanceInRadius;
// minimum and maximum longitudes for given distance
lsMinLongitude = void 0;
lsMaxLongitude = void 0;
// define deltaLon to help determine min and max longitudes
deltaLon = Math.asin(Math.sin(ldDistanceInRadius) / Math.cos(lsLatitudeInRadius));
if (lsMinLatitude > MIN_LAT && lsMaxLatitude < MAX_LAT) {
lsMinLongitude = lsLongitudeInRadius - deltaLon;
lsMaxLongitude = lsLongitudeInRadius + deltaLon;
if (lsMinLongitude < MIN_LON) {
lsMinLongitude = lsMinLongitude + 2 * Math.PI;
}
if (lsMaxLongitude > MAX_LON) {
lsMaxLongitude = lsMaxLongitude - 2 * Math.PI;
}
}
// a pole is within the given distance
else {
lsMinLatitude = Math.max(lsMinLatitude, MIN_LAT);
lsMaxLatitude = Math.min(lsMaxLatitude, MAX_LAT);
lsMinLongitude = MIN_LON;
lsMaxLongitude = MAX_LON;
}
return [
lsMinLatitude.radiusToDegree(),
lsMinLongitude.radiusToDegree(),
lsMaxLatitude.radiusToDegree(),
lsMaxLongitude.radiusToDegree()
];
};
use the below code to generate a lat/long array of rectanlge.
var lsRectangleLatLong = getBoundingBox(parseFloat(latitude), parseFloat(longitude), lsDistance);
if (lsRectangleLatLong != null && lsRectangleLatLong != undefined) {
latLngArr.push({ lat: lsRectangleLatLong[0], lng: lsRectangleLatLong[1] });
latLngArr.push({ lat: lsRectangleLatLong[0], lng: lsRectangleLatLong[3] });
latLngArr.push({ lat: lsRectangleLatLong[2], lng: lsRectangleLatLong[3] });
latLngArr.push({ lat: lsRectangleLatLong[2], lng: lsRectangleLatLong[1] });
}

Calculate zoom level from radius in android map v2

I've the radius calculated by the following formula:
public double getRadius() {
double latitudeSpan = 0;
VisibleRegion vr = mapView.getProjection().getVisibleRegion();
float [] results = new float[3];
Location.distanceBetween(vr.farLeft.latitude, vr.farLeft.longitude, vr.farRight.latitude, vr.farRight.longitude, results);
latitudeSpan = results[0];
return latitudeSpan;
}
but now I want to set the same zoom level using this radius later.
I calculated the zoom level by using this formula:
public static float zoom(double distance, Activity ctx) {
byte zoom = 1;
WindowManager windowManager = (WindowManager) ctx.getApplicationContext().getSystemService(Context.WINDOW_SERVICE);
int widthInPixels = windowManager.getDefaultDisplay().getWidth();
double equatorLength = 6378136.28;//in meters
double metersPerPixel = equatorLength / 256;
while ((metersPerPixel * widthInPixels) > distance) {
metersPerPixel /= 2;
++zoom;
}
if (zoom > 21)
zoom = 21;
if (zoom < 1)
zoom = 1;
return zoom;
}
But instead of getting zoom level in integer, how could I found the exact zoom level (in float) that was previously. Could any one help me please?

Find Intersection Point Between 2 LineStrings

I created a formula to form a grid on the google earth. I want to get the intersection point between the lat/long. Please tell me how we can get the intersection.I am using SharpKML lib for generating KML
for (int x = 90; x >= 0; x = x - 15)
{
Placemark placemark = new Placemark();
LineString line = new LineString();
CoordinateCollection co = new CoordinateCollection();
for (int i = 0; i <= 180; i = i + 15)
{
Vector cords = new Vector()
{
Latitude = x,
Longitude = i,
Altitude = 1000
};
co.Add(cords);
}
for (int i = -180; i <= 0; i = i + 15)
{
Vector cords = new Vector()
{
Latitude = x,
Longitude = i,
Altitude = 1000
};
co.Add(cords);
}
line.Coordinates = co;
placemark.Geometry = line;
document.AddFeature(placemark);
}
for (int x = -90; x <= 0; x = x + 15)
{
Placemark placemark = new Placemark();
LineString line = new LineString();
CoordinateCollection co = new CoordinateCollection();
for (int i = 0; i <= 180; i = i + 15)
{
Vector cords = new Vector()
{
Latitude = x,
Longitude = i,
Altitude = 1000
};
co.Add(cords);
}
for (int i = -180; i <= 0; i = i + 15)
{
Vector cords = new Vector()
{
Latitude = x,
Longitude = i,
Altitude = 1000
};
co.Add(cords);
}
line.Coordinates = co;
placemark.Geometry = line;
document.AddFeature(placemark);
}
for (int i = 0; i <= 180; i = i + 15)
{
Placemark placemark = new Placemark();
LineString line = new LineString();
CoordinateCollection co = new CoordinateCollection();
for (int x = 0; x <= 90; x = x + 15)
{
Vector cords = new Vector()
{
Latitude = x,
Longitude = i,
Altitude = 1000
};
co.Add(cords);
}
for (int x = -90; x <= 0; x = x + 15)
{
Vector cords = new Vector()
{
Latitude = x,
Longitude = i,
Altitude = 1000
};
co.Add(cords);
}
line.Coordinates = co;
placemark.Geometry = line;
document.AddFeature(placemark);
}
for (int i = -180; i <= 0; i = i + 15)
{
Placemark placemark = new Placemark();
LineString line = new LineString();
CoordinateCollection co = new CoordinateCollection();
for (int x = 0; x <= 90; x = x + 15)
{
Vector cords = new Vector()
{
Latitude = x,
Longitude = i,
Altitude = 1000
};
co.Add(cords);
}
for (int x = -90; x <= 0; x = x + 15)
{
Vector cords = new Vector()
{
Latitude = x,
Longitude = i,
Altitude = 1000
};
co.Add(cords);
}
line.Coordinates = co;
placemark.Geometry = line;
document.AddFeature(placemark);
}
Matthew is correct if the question is how you can find the intersection point of an arbitrary LineString object with your grid using C#. In C++ you can use GEOS http://trac.osgeo.org/geos/ in Java it would be JTS http://www.vividsolutions.com/jts/JTSHome.htm.
If, however, you are creating the grid yourself, and want an answer to the far simpler question of how do I find the intersection points between the horizontal and vertical lines of the grid I just created, the answer would be to use the same exact latitude, longitude values that you used for the LineStrings in a nested loop:
Document document = new Document();
for(y = -90; y < 0; y += 15){
for(x = -180; x < 0; x+= 15){
Point point = new Point();
point.Coordinate = new Vector(x, y);
Placemark placemark = new Placemark();
placemark.Geometry = point;
document.AddFeature(placemark);
}
}
.. repeat for the other 4 quadrants
// It's conventional for the root element to be Kml,
// but you could use document instead.
Kml root = new Kml();
root.Feature = document;
XmlFile kml = KmlFile.Create(root, false);
Here is some source code if you wanted to use DotSpatial, for instance, to find the intersection between the grids and a Shapefile. In this case, the shapefile has river lines and only produced one intersection point. Be advised that the topology intersection code is kind of slow, so you will want to use extent checking to speed things up. In your case you may want to build new Features by using KMLSharp to read a the linestring coordinates in a kml source file, rather than opening a shapefile, but the intersection code would be similar.
As a side note, I don't think the seemingly easy to use FeatureSet.Intersection method is smart enough to handle the case where line intersections produce point features as intersections. It only works for points or polygons where the output is likely to be the same feature type as the input.
using DotSpatial.Controls;
using DotSpatial.Data;
using DotSpatial.Topology;
using DotSpatial.Symbology;
private FeatureSet gridLines;
private void buttonAddGrid_Click(object sender, EventArgs e)
{
gridLines = new FeatureSet(FeatureType.Line);
for (int x = -180; x < 0; x += 15)
{
List<Coordinate> coords = new List<Coordinate>();
coords.Add(new Coordinate(x, -90));
coords.Add(new Coordinate(x, 90));
LineString ls = new LineString(coords);
gridLines.AddFeature(ls);
}
for (int y = -90; y < 0; y += 15)
{
List<Coordinate> coords = new List<Coordinate>();
coords.Add(new Coordinate(-180, y));
coords.Add(new Coordinate(180, y));
LineString ls = new LineString(coords);
gridLines.AddFeature(ls);
}
map1.Layers.Add(new MapLineLayer(gridLines));
}
private void buttonIntersect_Click(object sender, EventArgs e)
{
if (gridLines == null)
{
MessageBox.Show("First add the grid.");
}
IFeatureSet river = FeatureSet.Open(#"C:\Data\Rivers\River.shp");
MapLineLayer riverLayer = new MapLineLayer(river);
map1.Layers.Add(river);
List<DotSpatial.Topology.Point> allResultPoints = new List<DotSpatial.Topology.Point>();
foreach (Feature polygon in river.Features)
{
Geometry lineString = polygon.BasicGeometry as Geometry;
foreach (Feature lineFeature in gridLines.Features)
{
// Speed up calculation with extent testing.
if(!lineFeature.Envelope.Intersects(lineString.Envelope)){
continue;
}
IFeature intersectFeature = lineFeature.Intersection(lineString);
if (intersectFeature == null)
{
continue;
}
MultiPoint multi = intersectFeature.BasicGeometry as MultiPoint;
if (multi != null)
{
for(int i = 0; i < multi.NumGeometries; i++)
{
allResultPoints.Add(intersectFeature.GetBasicGeometryN(i) as DotSpatial.Topology.Point);
}
}
DotSpatial.Topology.Point single = intersectFeature.BasicGeometry as DotSpatial.Topology.Point;
{
allResultPoints.Add(single);
}
}
}
FeatureSet finalPoints = new FeatureSet(FeatureType.Point);
foreach(DotSpatial.Topology.Point pt in allResultPoints){
finalPoints.AddFeature(pt);
}
map1.Layers.Add(new MapPointLayer(finalPoints));
}
I think the DotSpatial library should meet your needs, I have used this library in the past but not made use of the intersections function:
http://dotspatial.codeplex.com/wikipage?title=DotSpatial.Data.FeatureSetExt.Intersection
If you try and do your own line intersection analysis, know that a simplistic Cartesian plane approach will introduce errors (which [I think] become more obvious as you approach the poles).
See here: http://www.geog.ubc.ca/courses/klink/gis.notes/ncgia/u32.html
And here: Intersection between two geographic lines

Center and zoom Android MapView around a group of markers?

I have a mapView with several points plotted with markers, each with different latitudes and longitudes that may or may not be near each other in vicinity. Is there a way I can have the MapView open to the deepest zoom level that centers all the points on the screen together?
I know the mapController has a setZoom and setCenter property, but how can I determine the values for those methods? Or is there another simpler way to accomplish this?
In your MapView, after you have added your overlayItems in your ImtemizedOverlay, like this:
GeoPoint aGeoPoint = new GeoPoint((int)(latitude * 1E6), (int)(longitude * 1E6));
OverlayItem aOverlayItem = new OverlayItem(aGeoPoint, "Auto", "Auto");
locationsItemizedOverlay.addOverlay(aOverlayItem);
Try this:
// Center map in geopoints
GeoPoint point = locationsItemizedOverlay.getCenterPoint();
int latSpan = locationsItemizedOverlay.getLatSpanE6();
int longSpan = locationsItemizedOverlay.getLonSpanE6();
mapView.getController().setCenter(point);
mapView.getController().zoomToSpan((int)(latSpan * Constants.MAP_ZOOM_FACTOR), (int)(longSpan * Constants.MAP_ZOOM_FACTOR));
In your overriden ItemizedOverlay:
public GeoPoint getCenterPoint() {
if(center == null) {
int northEdge = -90000000; // i.e., -90E6 microdegrees
int southEdge = 90000000;
int eastEdge = -180000000;
int westEdge = 180000000;
Iterator<OverlayItem> iterator = overlayItemsArrayList.iterator();
while(iterator.hasNext()) {
GeoPoint geoPoint = iterator.next().getPoint();
if(geoPoint.getLatitudeE6() > northEdge)
northEdge = geoPoint.getLatitudeE6();
if(geoPoint.getLatitudeE6() < southEdge)
southEdge = geoPoint.getLatitudeE6();
if(geoPoint.getLongitudeE6() > eastEdge)
eastEdge = geoPoint.getLongitudeE6();
if(geoPoint.getLongitudeE6() < westEdge)
westEdge = geoPoint.getLongitudeE6();
}
center = new GeoPoint((int)((northEdge + southEdge) / 2), (int)((westEdge + eastEdge) / 2));
}
return center;
}
I hope it helps.

Google Maps V3 - How to calculate the zoom level for a given bounds

I'm looking for a way to calculate the zoom level for a given bounds using the Google Maps V3 API, similar to getBoundsZoomLevel() in the V2 API.
Here is what I want to do:
// These are exact bounds previously captured from the map object
var sw = new google.maps.LatLng(42.763479, -84.338918);
var ne = new google.maps.LatLng(42.679488, -84.524313);
var bounds = new google.maps.LatLngBounds(sw, ne);
var zoom = // do some magic to calculate the zoom level
// Set the map to these exact bounds
map.setCenter(bounds.getCenter());
map.setZoom(zoom);
// NOTE: fitBounds() will not work
Unfortunately, I can't use the fitBounds() method for my particular use case. It works well for fitting markers on the map, but it does not work well for setting exact bounds. Here is an example of why I can't use the fitBounds() method.
map.fitBounds(map.getBounds()); // not what you expect
Thanks to Giles Gardam for his answer, but it addresses only longitude and not latitude. A complete solution should calculate the zoom level needed for latitude and the zoom level needed for longitude, and then take the smaller (further out) of the two.
Here is a function that uses both latitude and longitude:
function getBoundsZoomLevel(bounds, mapDim) {
var WORLD_DIM = { height: 256, width: 256 };
var ZOOM_MAX = 21;
function latRad(lat) {
var sin = Math.sin(lat * Math.PI / 180);
var radX2 = Math.log((1 + sin) / (1 - sin)) / 2;
return Math.max(Math.min(radX2, Math.PI), -Math.PI) / 2;
}
function zoom(mapPx, worldPx, fraction) {
return Math.floor(Math.log(mapPx / worldPx / fraction) / Math.LN2);
}
var ne = bounds.getNorthEast();
var sw = bounds.getSouthWest();
var latFraction = (latRad(ne.lat()) - latRad(sw.lat())) / Math.PI;
var lngDiff = ne.lng() - sw.lng();
var lngFraction = ((lngDiff < 0) ? (lngDiff + 360) : lngDiff) / 360;
var latZoom = zoom(mapDim.height, WORLD_DIM.height, latFraction);
var lngZoom = zoom(mapDim.width, WORLD_DIM.width, lngFraction);
return Math.min(latZoom, lngZoom, ZOOM_MAX);
}
Demo on jsfiddle
Parameters:
The "bounds" parameter value should be a google.maps.LatLngBounds object.
The "mapDim" parameter value should be an object with "height" and "width" properties that represent the height and width of the DOM element that displays the map. You may want to decrease these values if you want to ensure padding. That is, you may not want map markers within the bounds to be too close to the edge of the map.
If you are using the jQuery library, the mapDim value can be obtained as follows:
var $mapDiv = $('#mapElementId');
var mapDim = { height: $mapDiv.height(), width: $mapDiv.width() };
If you are using the Prototype library, the mapDim value can be obtained as follows:
var mapDim = $('mapElementId').getDimensions();
Return Value:
The return value is the maximum zoom level that will still display the entire bounds. This value will be between 0 and the maximum zoom level, inclusive.
The maximum zoom level is 21. (I believe it was only 19 for Google Maps API v2.)
Explanation:
Google Maps uses a Mercator projection. In a Mercator projection the lines of longitude are equally spaced, but the lines of latitude are not. The distance between lines of latitude increase as they go from the equator to the poles. In fact the distance tends towards infinity as it reaches the poles. A Google Maps map, however, does not show latitudes above approximately 85 degrees North or below approximately -85 degrees South. (reference) (I calculate the actual cutoff at +/-85.05112877980658 degrees.)
This makes the calculation of the fractions for the bounds more complicated for latitude than for longitude. I used a formula from Wikipedia to calculate the latitude fraction. I am assuming this matches the projection used by Google Maps. After all, the Google Maps documentation page I link to above contains a link to the same Wikipedia page.
Other Notes:
Zoom levels range from 0 to the maximum zoom level. Zoom level 0 is the map fully zoomed out. Higher levels zoom the map in further. (reference)
At zoom level 0 the entire world can be displayed in an area that is 256 x 256 pixels. (reference)
For each higher zoom level the number of pixels needed to display the same area doubles in both width and height. (reference)
Maps wrap in the longitudinal direction, but not in the latitudinal direction.
A similar question has been asked on the Google group: http://groups.google.com/group/google-maps-js-api-v3/browse_thread/thread/e6448fc197c3c892
The zoom levels are discrete, with the scale doubling in each step. So in general you cannot fit the bounds you want exactly (unless you are very lucky with the particular map size).
Another issue is the ratio between side lengths e.g. you cannot fit the bounds exactly to a thin rectangle inside a square map.
There's no easy answer for how to fit exact bounds, because even if you are willing to change the size of the map div, you have to choose which size and corresponding zoom level you change to (roughly speaking, do you make it larger or smaller than it currently is?).
If you really need to calculate the zoom, rather than store it, this should do the trick:
The Mercator projection warps latitude, but any difference in longitude always represents the same fraction of the width of the map (the angle difference in degrees / 360). At zoom zero, the whole world map is 256x256 pixels, and zooming each level doubles both width and height. So after a little algebra we can calculate the zoom as follows, provided we know the map's width in pixels. Note that because longitude wraps around, we have to make sure the angle is positive.
var GLOBE_WIDTH = 256; // a constant in Google's map projection
var west = sw.lng();
var east = ne.lng();
var angle = east - west;
if (angle < 0) {
angle += 360;
}
var zoom = Math.round(Math.log(pixelWidth * 360 / angle / GLOBE_WIDTH) / Math.LN2);
For version 3 of the API, this is simple and working:
var latlngList = [];
latlngList.push(new google.maps.LatLng(lat, lng));
var bounds = new google.maps.LatLngBounds();
latlngList.each(function(n) {
bounds.extend(n);
});
map.setCenter(bounds.getCenter()); //or use custom center
map.fitBounds(bounds);
and some optional tricks:
//remove one zoom level to ensure no marker is on the edge.
map.setZoom(map.getZoom() - 1);
// set a minimum zoom
// if you got only 1 marker or all markers are on the same address map will be zoomed too much.
if(map.getZoom() > 15){
map.setZoom(15);
}
Dart Version:
double latRad(double lat) {
final double sin = math.sin(lat * math.pi / 180);
final double radX2 = math.log((1 + sin) / (1 - sin)) / 2;
return math.max(math.min(radX2, math.pi), -math.pi) / 2;
}
double getMapBoundZoom(LatLngBounds bounds, double mapWidth, double mapHeight) {
final LatLng northEast = bounds.northEast;
final LatLng southWest = bounds.southWest;
final double latFraction = (latRad(northEast.latitude) - latRad(southWest.latitude)) / math.pi;
final double lngDiff = northEast.longitude - southWest.longitude;
final double lngFraction = ((lngDiff < 0) ? (lngDiff + 360) : lngDiff) / 360;
final double latZoom = (math.log(mapHeight / 256 / latFraction) / math.ln2).floorToDouble();
final double lngZoom = (math.log(mapWidth / 256 / lngFraction) / math.ln2).floorToDouble();
return math.min(latZoom, lngZoom);
}
Here a Kotlin version of the function:
fun getBoundsZoomLevel(bounds: LatLngBounds, mapDim: Size): Double {
val WORLD_DIM = Size(256, 256)
val ZOOM_MAX = 21.toDouble();
fun latRad(lat: Double): Double {
val sin = Math.sin(lat * Math.PI / 180);
val radX2 = Math.log((1 + sin) / (1 - sin)) / 2;
return max(min(radX2, Math.PI), -Math.PI) /2
}
fun zoom(mapPx: Int, worldPx: Int, fraction: Double): Double {
return floor(Math.log(mapPx / worldPx / fraction) / Math.log(2.0))
}
val ne = bounds.northeast;
val sw = bounds.southwest;
val latFraction = (latRad(ne.latitude) - latRad(sw.latitude)) / Math.PI;
val lngDiff = ne.longitude - sw.longitude;
val lngFraction = if (lngDiff < 0) { (lngDiff + 360) / 360 } else { (lngDiff / 360) }
val latZoom = zoom(mapDim.height, WORLD_DIM.height, latFraction);
val lngZoom = zoom(mapDim.width, WORLD_DIM.width, lngFraction);
return minOf(latZoom, lngZoom, ZOOM_MAX)
}
None of the highly upvoted answers worked for me. They threw various undefined errors and ended up calculating inf/nan for angles. I suspect perhaps the behavior of LatLngBounds has changed over time. In any case, I found this code to work for my needs, perhaps it can help someone:
function latRad(lat) {
var sin = Math.sin(lat * Math.PI / 180);
var radX2 = Math.log((1 + sin) / (1 - sin)) / 2;
return Math.max(Math.min(radX2, Math.PI), -Math.PI) / 2;
}
function getZoom(lat_a, lng_a, lat_b, lng_b) {
let latDif = Math.abs(latRad(lat_a) - latRad(lat_b))
let lngDif = Math.abs(lng_a - lng_b)
let latFrac = latDif / Math.PI
let lngFrac = lngDif / 360
let lngZoom = Math.log(1/latFrac) / Math.log(2)
let latZoom = Math.log(1/lngFrac) / Math.log(2)
return Math.min(lngZoom, latZoom)
}
Thanks, that helped me a lot in finding the most suitable zoom factor to correctly display a polyline.
I find the maximum and minimum coordinates among the points I have to track and, in case the path is very "vertical", I just added few lines of code:
var GLOBE_WIDTH = 256; // a constant in Google's map projection
var west = <?php echo $minLng; ?>;
var east = <?php echo $maxLng; ?>;
*var north = <?php echo $maxLat; ?>;*
*var south = <?php echo $minLat; ?>;*
var angle = east - west;
if (angle < 0) {
angle += 360;
}
*var angle2 = north - south;*
*if (angle2 > angle) angle = angle2;*
var zoomfactor = Math.round(Math.log(960 * 360 / angle / GLOBE_WIDTH) / Math.LN2);
Actually, the ideal zoom factor is zoomfactor-1.
Since all of the other answers seem to have issues for me with one or another set of circumstances (map width/height, bounds width/height, etc.) I figured I'd put my answer here...
There was a very useful javascript file here: http://www.polyarc.us/adjust.js
I used that as a base for this:
var com = com || {};
com.local = com.local || {};
com.local.gmaps3 = com.local.gmaps3 || {};
com.local.gmaps3.CoordinateUtils = new function() {
var OFFSET = 268435456;
var RADIUS = OFFSET / Math.PI;
/**
* Gets the minimum zoom level that entirely contains the Lat/Lon bounding rectangle given.
*
* #param {google.maps.LatLngBounds} boundary the Lat/Lon bounding rectangle to be contained
* #param {number} mapWidth the width of the map in pixels
* #param {number} mapHeight the height of the map in pixels
* #return {number} the minimum zoom level that entirely contains the given Lat/Lon rectangle boundary
*/
this.getMinimumZoomLevelContainingBounds = function ( boundary, mapWidth, mapHeight ) {
var zoomIndependentSouthWestPoint = latLonToZoomLevelIndependentPoint( boundary.getSouthWest() );
var zoomIndependentNorthEastPoint = latLonToZoomLevelIndependentPoint( boundary.getNorthEast() );
var zoomIndependentNorthWestPoint = { x: zoomIndependentSouthWestPoint.x, y: zoomIndependentNorthEastPoint.y };
var zoomIndependentSouthEastPoint = { x: zoomIndependentNorthEastPoint.x, y: zoomIndependentSouthWestPoint.y };
var zoomLevelDependentSouthEast, zoomLevelDependentNorthWest, zoomLevelWidth, zoomLevelHeight;
for( var zoom = 21; zoom >= 0; --zoom ) {
zoomLevelDependentSouthEast = zoomLevelIndependentPointToMapCanvasPoint( zoomIndependentSouthEastPoint, zoom );
zoomLevelDependentNorthWest = zoomLevelIndependentPointToMapCanvasPoint( zoomIndependentNorthWestPoint, zoom );
zoomLevelWidth = zoomLevelDependentSouthEast.x - zoomLevelDependentNorthWest.x;
zoomLevelHeight = zoomLevelDependentSouthEast.y - zoomLevelDependentNorthWest.y;
if( zoomLevelWidth <= mapWidth && zoomLevelHeight <= mapHeight )
return zoom;
}
return 0;
};
function latLonToZoomLevelIndependentPoint ( latLon ) {
return { x: lonToX( latLon.lng() ), y: latToY( latLon.lat() ) };
}
function zoomLevelIndependentPointToMapCanvasPoint ( point, zoomLevel ) {
return {
x: zoomLevelIndependentCoordinateToMapCanvasCoordinate( point.x, zoomLevel ),
y: zoomLevelIndependentCoordinateToMapCanvasCoordinate( point.y, zoomLevel )
};
}
function zoomLevelIndependentCoordinateToMapCanvasCoordinate ( coordinate, zoomLevel ) {
return coordinate >> ( 21 - zoomLevel );
}
function latToY ( lat ) {
return OFFSET - RADIUS * Math.log( ( 1 + Math.sin( lat * Math.PI / 180 ) ) / ( 1 - Math.sin( lat * Math.PI / 180 ) ) ) / 2;
}
function lonToX ( lon ) {
return OFFSET + RADIUS * lon * Math.PI / 180;
}
};
You can certainly clean this up or minify it if needed, but I kept the variable names long in an attempt to make it easier to understand.
If you are wondering where OFFSET came from, apparently 268435456 is half of earth's circumference in pixels at zoom level 21 (according to http://www.appelsiini.net/2008/11/introduction-to-marker-clustering-with-google-maps).
Valerio is almost right with his solution, but there is some logical mistake.
you must firstly check wether angle2 is bigger than angle, before adding 360 at a negative.
otherwise you always have a bigger value than angle
So the correct solution is:
var west = calculateMin(data.longitudes);
var east = calculateMax(data.longitudes);
var angle = east - west;
var north = calculateMax(data.latitudes);
var south = calculateMin(data.latitudes);
var angle2 = north - south;
var zoomfactor;
var delta = 0;
var horizontal = false;
if(angle2 > angle) {
angle = angle2;
delta = 3;
}
if (angle < 0) {
angle += 360;
}
zoomfactor = Math.floor(Math.log(960 * 360 / angle / GLOBE_WIDTH) / Math.LN2) - 2 - delta;
Delta is there, because i have a bigger width than height.
map.getBounds() is not momentary operation, so I use in similar case event handler. Here is my example in Coffeescript
#map.fitBounds(#bounds)
google.maps.event.addListenerOnce #map, 'bounds_changed', =>
#map.setZoom(12) if #map.getZoom() > 12
Work example to find average default center with react-google-maps on ES6:
const bounds = new google.maps.LatLngBounds();
paths.map((latLng) => bounds.extend(new google.maps.LatLng(latLng)));
const defaultCenter = bounds.getCenter();
<GoogleMap
defaultZoom={paths.length ? 12 : 4}
defaultCenter={defaultCenter}
>
<Marker position={{ lat, lng }} />
</GoogleMap>
The calculation of the zoom level for the longitudes of Giles Gardam works fine for me.
If you want to calculate the zoom factor for latitude, this is an easy solution that works fine:
double minLat = ...;
double maxLat = ...;
double midAngle = (maxLat+minLat)/2;
//alpha is the non-negative angle distance of alpha and beta to midangle
double alpha = maxLat-midAngle;
//Projection screen is orthogonal to vector with angle midAngle
//portion of horizontal scale:
double yPortion = Math.sin(alpha*Math.pi/180) / 2;
double latZoom = Math.log(mapSize.height / GLOBE_WIDTH / yPortion) / Math.ln2;
//return min (max zoom) of both zoom levels
double zoom = Math.min(lngZoom, latZoom);
For swift version
func getBoundsZoomLevel(bounds: GMSCoordinateBounds, mapDim: CGSize) -> Double {
var bounds = bounds
let WORLD_DIM = CGSize(width: 256, height: 256)
let ZOOM_MAX: Double = 21.0
func latRad(_ lat: Double) -> Double {
let sin2 = sin(lat * .pi / 180)
let radX2 = log10((1 + sin2) / (1 - sin2)) / 2
return max(min(radX2, .pi), -.pi) / 2
}
func zoom(_ mapPx: CGFloat,_ worldPx: CGFloat,_ fraction: Double) -> Double {
return floor(log10(Double(mapPx) / Double(worldPx) / fraction / log10(2.0)))
}
let ne = bounds.northEast
let sw = bounds.southWest
let latFraction = (latRad(ne.latitude) - latRad(sw.latitude)) / .pi
let lngDiff = ne.longitude - sw.longitude
let lngFraction = lngDiff < 0 ? (lngDiff + 360) : (lngDiff / 360)
let latZoom = zoom(mapDim.height, WORLD_DIM.height, latFraction);
let lngZoom = zoom(mapDim.width, WORLD_DIM.width, lngFraction);
return min(latZoom, lngZoom, ZOOM_MAX)
}
Calculate zoom level to display a map including the two cross corners of the area and display the map on a the part of the screen with a specific height.
Two coordinates
max lat/long
min lat/long
Display area in pixels
height
double getZoomLevelNew(context,
double maxLat, double maxLong,
double minLat, double minLong,
double height){
try {
double _zoom;
MediaQueryData queryData2;
queryData2 = MediaQuery.of(context);
double _zLat =
Math.log(
(globals.factor(height) / queryData2.devicePixelRatio / 256.0) *
180 / (maxLat - minLat).abs()) / Math.log(2);
double _zLong =
Math.log((globals.factor(MediaQuery
.of(context)
.size
.width) / queryData2.devicePixelRatio / 256.0) * 360 /
(maxLong - minLong).abs()) / Math.log(2);
_zoom = Math.min(_zLat, _zLong)*globals.zoomFactorNew;
if (_zoom < 0) {
_zoom = 0;
}
return _zoom;
} catch(e){
print("getZoomLevelNew - excep - " + e.toString());
}