How to get bounds in degrees of google static map which has been returned, for example, for following request
http://maps.googleapis.com/maps/api/staticmap?center=0.0,0.0&zoom=10&size=640x640&sensor=false
As I know, full Earth map is 256x256 image. This means that n vertical pixels contain x degrees, but n horizontal pixels contain 2x degrees. Right?
As google says
center defines the center of the map, equidistant from all edges of the map. As I understood equidistant in pixels (or in degrees?). And each succeeding zoom level doubles the precision in both horizontal and vertical dimensions.
So, I can find delta value of Longitude of map for each zoom value as:
dLongitude = (HorizontalMapSizeInPixels / 256 ) * ( 360 / pow(2, zoom) );
Same calculations for Latitude:
dLatitude = (VerticalMapSizeInPixels / 256 ) * ( 180 / pow(2, zoom) );
VerticalMapSizeInPixels and HorizontalMapSizeInPixels are parameters of map size in URL.
It's good to calculate delta value of Longitude, but for Latitude it is wrong. I cannot find delta value of Latitude, there is some delta error.
As I know, full Earth map is 256x256 image.
Yes.
This means that n vertical pixels contain x degrees, but n horizontal
pixels contain 2x degrees. Right?
No. One pixel will represent varying amounts of latitude depending on the latitude. One pixel at the Equator represents less latitude than one pixel near the poles.
The corners of the map will depend on center, zoom level and map size, and you'd need to use the Mercator projection to calculate them.
If you don't want to load the full API, here's a MercatorProjection object:
var MERCATOR_RANGE = 256;
function bound(value, opt_min, opt_max) {
if (opt_min != null) value = Math.max(value, opt_min);
if (opt_max != null) value = Math.min(value, opt_max);
return value;
}
function degreesToRadians(deg) {
return deg * (Math.PI / 180);
}
function radiansToDegrees(rad) {
return rad / (Math.PI / 180);
}
function MercatorProjection() {
this.pixelOrigin_ = new google.maps.Point( MERCATOR_RANGE / 2, MERCATOR_RANGE / 2);
this.pixelsPerLonDegree_ = MERCATOR_RANGE / 360;
this.pixelsPerLonRadian_ = MERCATOR_RANGE / (2 * Math.PI);
};
MercatorProjection.prototype.fromLatLngToPoint = function(latLng, opt_point) {
var me = this;
var point = opt_point || new google.maps.Point(0, 0);
var origin = me.pixelOrigin_;
point.x = origin.x + latLng.lng() * me.pixelsPerLonDegree_;
// NOTE(appleton): Truncating to 0.9999 effectively limits latitude to
// 89.189. This is about a third of a tile past the edge of the world tile.
var siny = bound(Math.sin(degreesToRadians(latLng.lat())), -0.9999, 0.9999);
point.y = origin.y + 0.5 * Math.log((1 + siny) / (1 - siny)) * -me.pixelsPerLonRadian_;
return point;
};
MercatorProjection.prototype.fromPointToLatLng = function(point) {
var me = this;
var origin = me.pixelOrigin_;
var lng = (point.x - origin.x) / me.pixelsPerLonDegree_;
var latRadians = (point.y - origin.y) / -me.pixelsPerLonRadian_;
var lat = radiansToDegrees(2 * Math.atan(Math.exp(latRadians)) - Math.PI / 2);
return new google.maps.LatLng(lat, lng);
};
//pixelCoordinate = worldCoordinate * Math.pow(2,zoomLevel)
You can save that to a separate file, for example "MercatorProjection.js", and then include it in your application.
<script src="MercatorProjection.js"></script>
With the above file loaded, the following function calculates the SW and NE corners of a map of a given size and at a given zoom.
function getCorners(center,zoom,mapWidth,mapHeight){
var scale = Math.pow(2,zoom);
var centerPx = proj.fromLatLngToPoint(center);
var SWPoint = {x: (centerPx.x -(mapWidth/2)/ scale) , y: (centerPx.y + (mapHeight/2)/ scale)};
var SWLatLon = proj.fromPointToLatLng(SWPoint);
alert('SW: ' + SWLatLon);
var NEPoint = {x: (centerPx.x +(mapWidth/2)/ scale) , y: (centerPx.y - (mapHeight/2)/ scale)};
var NELatLon = proj.fromPointToLatLng(NEPoint);
alert(' NE: '+ NELatLon);
}
and you'd call it like this:
var proj = new MercatorProjection();
var G = google.maps;
var centerPoint = new G.LatLng(49.141404, -121.960988);
var zoom = 10;
getCorners(centerPoint,zoom,640,640);
Thanks Marcelo for your answer. It has been quite helpful. In case anyone would be interested, here the Python version of the code (a rough translation of the PHP code, probably not as pythonic as it could be):
from __future__ import division
import math
MERCATOR_RANGE = 256
def bound(value, opt_min, opt_max):
if (opt_min != None):
value = max(value, opt_min)
if (opt_max != None):
value = min(value, opt_max)
return value
def degreesToRadians(deg) :
return deg * (math.pi / 180)
def radiansToDegrees(rad) :
return rad / (math.pi / 180)
class G_Point :
def __init__(self,x=0, y=0):
self.x = x
self.y = y
class G_LatLng :
def __init__(self,lt, ln):
self.lat = lt
self.lng = ln
class MercatorProjection :
def __init__(self) :
self.pixelOrigin_ = G_Point( MERCATOR_RANGE / 2, MERCATOR_RANGE / 2)
self.pixelsPerLonDegree_ = MERCATOR_RANGE / 360
self.pixelsPerLonRadian_ = MERCATOR_RANGE / (2 * math.pi)
def fromLatLngToPoint(self, latLng, opt_point=None) :
point = opt_point if opt_point is not None else G_Point(0,0)
origin = self.pixelOrigin_
point.x = origin.x + latLng.lng * self.pixelsPerLonDegree_
# NOTE(appleton): Truncating to 0.9999 effectively limits latitude to
# 89.189. This is about a third of a tile past the edge of the world tile.
siny = bound(math.sin(degreesToRadians(latLng.lat)), -0.9999, 0.9999)
point.y = origin.y + 0.5 * math.log((1 + siny) / (1 - siny)) * - self.pixelsPerLonRadian_
return point
def fromPointToLatLng(self,point) :
origin = self.pixelOrigin_
lng = (point.x - origin.x) / self.pixelsPerLonDegree_
latRadians = (point.y - origin.y) / -self.pixelsPerLonRadian_
lat = radiansToDegrees(2 * math.atan(math.exp(latRadians)) - math.pi / 2)
return G_LatLng(lat, lng)
#pixelCoordinate = worldCoordinate * pow(2,zoomLevel)
def getCorners(center, zoom, mapWidth, mapHeight):
scale = 2**zoom
proj = MercatorProjection()
centerPx = proj.fromLatLngToPoint(center)
SWPoint = G_Point(centerPx.x-(mapWidth/2)/scale, centerPx.y+(mapHeight/2)/scale)
SWLatLon = proj.fromPointToLatLng(SWPoint)
NEPoint = G_Point(centerPx.x+(mapWidth/2)/scale, centerPx.y-(mapHeight/2)/scale)
NELatLon = proj.fromPointToLatLng(NEPoint)
return {
'N' : NELatLon.lat,
'E' : NELatLon.lng,
'S' : SWLatLon.lat,
'W' : SWLatLon.lng,
}
Usage :
>>> import MercatorProjection
>>> centerLat = 49.141404
>>> centerLon = -121.960988
>>> zoom = 10
>>> mapWidth = 640
>>> mapHeight = 640
>>> centerPoint = MercatorProjection.G_LatLng(centerLat, centerLon)
>>> corners = MercatorProjection.getCorners(centerPoint, zoom, mapWidth, mapHeight)
>>> corners
{'E': -65.710988,
'N': 74.11120692972199,
'S': 0.333879313530149,
'W': -178.210988}
>>> mapURL = "http://maps.googleapis.com/maps/api/staticmap?center=%f,%f&zoom=%d&size=%dx%d&scale=2&maptype=roadmap&sensor=false"%(centerLat,centerLon,zoom,mapWidth,mapHeight)
>>> mapURL
http://maps.googleapis.com/maps/api/staticmap?center=49.141404,-121.960988&zoom=10&size=640x640&scale=2&maptype=roadmap&sensor=false'
Simple Python Version
Having spent a long time on a similar problem and using this thread for help, here is a simplified Python version of Marcelo (and Jmague)'s code:
import math
import requests
def latLngToPoint(mapWidth, mapHeight, lat, lng):
x = (lng + 180) * (mapWidth/360)
y = ((1 - math.log(math.tan(lat * math.pi / 180) + 1 / math.cos(lat * math.pi / 180)) / math.pi) / 2) * mapHeight
return(x, y)
def pointToLatLng(mapWidth, mapHeight, x, y):
lng = x / mapWidth * 360 - 180
n = math.pi - 2 * math.pi * y / mapHeight
lat = (180 / math.pi * math. atan(0.5 * (math.exp(n) - math.exp(-n))))
return(lat, lng)
def getImageBounds(mapWidth, mapHeight, xScale, yScale, lat, lng):
centreX, centreY = latLngToPoint(mapWidth, mapHeight, lat, lng)
southWestX = centreX - (mapWidth/2)/ xScale
southWestY = centreY + (mapHeight/2)/ yScale
SWlat, SWlng = pointToLatLng(mapWidth, mapHeight, southWestX, southWestY)
northEastX = centreX + (mapWidth/2)/ xScale
northEastY = centreY - (mapHeight/2)/ yScale
NElat, NElng = pointToLatLng(mapWidth, mapHeight, northEastX, northEastY)
return[SWlat, SWlng, NElat, NElng]
lat = 37.806716
lng = -122.477702
zoom = 16
picHeight = 640 #Resulting image height in pixels (x2 if scale parameter is set to 2)
picWidth = 640
mapHeight = 256 #Original map size - specific to Google Maps
mapWidth = 256
xScale = math.pow(2, zoom) / (picWidth/mapWidth)
yScale = math.pow(2, zoom) / (picHeight/mapWidth)
corners = getImageBounds(mapWidth, mapHeight, xScale, yScale, lat, lng)
Here I have used x and y to represent pixel values and lat lng as, Latitude and Longitude. lat, lng, zoom, picHeight and picWidth can all be changed to your specific use case. Changing the scale/ maptype etc will not affect this calculation.
I used this code to tile Static Maps images with no gaps/ overlap. If you want to see more of it in use/ how it can work in that sense there is more information on my GitHub.
Here is a line by line translation of Marcelo's code in PHP, which can probably be cleaned up a bit. Works great! Thanks to Marcelo for doing the hard part.
define("MERCATOR_RANGE", 256);
function degreesToRadians($deg) {
return $deg * (M_PI / 180);
}
function radiansToDegrees($rad) {
return $rad / (M_PI / 180);
}
function bound($value, $opt_min, $opt_max) {
if ($opt_min != null) $value = max($value, $opt_min);
if ($opt_max != null) $value = min($value, $opt_max);
return $value;
}
class G_Point {
public $x,$y;
function G_Point($x=0, $y=0){
$this->x = $x;
$this->y = $y;
}
}
class G_LatLng {
public $lat,$lng;
function G_LatLng($lt, $ln){
$this->lat = $lt;
$this->lng = $ln;
}
}
class MercatorProjection {
private $pixelOrigin_, $pixelsPerLonDegree_, $pixelsPerLonRadian_;
function MercatorProjection() {
$this->pixelOrigin_ = new G_Point( MERCATOR_RANGE / 2, MERCATOR_RANGE / 2);
$this->pixelsPerLonDegree_ = MERCATOR_RANGE / 360;
$this->pixelsPerLonRadian_ = MERCATOR_RANGE / (2 * M_PI);
}
public function fromLatLngToPoint($latLng, $opt_point=null) {
$me = $this;
$point = $opt_point ? $opt_point : new G_Point(0,0);
$origin = $me->pixelOrigin_;
$point->x = $origin->x + $latLng->lng * $me->pixelsPerLonDegree_;
// NOTE(appleton): Truncating to 0.9999 effectively limits latitude to
// 89.189. This is about a third of a tile past the edge of the world tile.
$siny = bound(sin(degreesToRadians($latLng->lat)), -0.9999, 0.9999);
$point->y = $origin->y + 0.5 * log((1 + $siny) / (1 - $siny)) * -$me->pixelsPerLonRadian_;
return $point;
}
public function fromPointToLatLng($point) {
$me = $this;
$origin = $me->pixelOrigin_;
$lng = ($point->x - $origin->x) / $me->pixelsPerLonDegree_;
$latRadians = ($point->y - $origin->y) / -$me->pixelsPerLonRadian_;
$lat = radiansToDegrees(2 * atan(exp($latRadians)) - M_PI / 2);
return new G_LatLng($lat, $lng);
}
//pixelCoordinate = worldCoordinate * pow(2,zoomLevel)
}
function getCorners($center, $zoom, $mapWidth, $mapHeight){
$scale = pow(2, $zoom);
$proj = new MercatorProjection();
$centerPx = $proj->fromLatLngToPoint($center);
$SWPoint = new G_Point($centerPx->x-($mapWidth/2)/$scale, $centerPx->y+($mapHeight/2)/$scale);
$SWLatLon = $proj->fromPointToLatLng($SWPoint);
$NEPoint = new G_Point($centerPx->x+($mapWidth/2)/$scale, $centerPx->y-($mapHeight/2)/$scale);
$NELatLon = $proj->fromPointToLatLng($NEPoint);
return array(
'N' => $NELatLon->lat,
'E' => $NELatLon->lng,
'S' => $SWLatLon->lat,
'W' => $SWLatLon->lng,
);
}
Usage:
$centerLat = 49.141404;
$centerLon = -121.960988;
$zoom = 10;
$mapWidth = 640;
$mapHeight = 640;
$centerPoint = new G_LatLng($centerLat, $centerLon);
$corners = getCorners($centerPoint, $zoom, $mapWidth, $mapHeight);
$mapURL = "http://maps.googleapis.com/maps/api/staticmap?center={$centerLat},{$centerLon}&zoom={$zoom}&size={$mapWidth}x{$mapHeight}&scale=2&maptype=roadmap&sensor=false";
Here is translation to Delphi/Pascal with some optimizations to correspond more strict Pascal language and memory management.
unit Mercator.Google.Maps;
interface
uses System.Math;
type TG_Point = class(TObject)
private
Fx: integer;
Fy: integer;
public
property x: integer read Fx write Fx;
property y: integer read Fy write Fy;
constructor Create(Ax: integer = 0; Ay: integer = 0);
end;
type TG_LatLng = class(TObject)
private
FLat: double;
FLng: double;
public
property Lat: double read FLat write FLat;
property Lng: double read FLng write FLng;
constructor Create(ALat: double; ALng: double);
end;
type TMercatorProjection = class(TObject)
private
pixelOrigin_: TG_Point;
pixelsPerLonDegree_, pixelsPerLonRadian_: double;
function degreesToRadians(deg: double): double;
function radiansToDegrees(rad: double): double;
function bound(value: double; opt_min: double; opt_max: double): double;
public
constructor Create;
procedure fromLatLngToPoint(latLng: TG_LatLng; var point: TG_Point);
procedure fromPointToLatLng(point: TG_point; var latLng: TG_LatLng);
procedure getCorners(center: TG_LatLng; zoom: integer; mapWidth: integer; mapHeight: integer;
var NELatLon: TG_LatLng; var SWLatLon: TG_LatLng);
end;
implementation
const MERCATOR_RANGE = 256;
constructor TG_Point.Create(Ax: Integer = 0; Ay: Integer = 0);
begin
inherited Create;
Fx := Ax;
Fy := Ay
end;
// **************
constructor TG_LatLng.Create(ALat: double; ALng: double);
begin
inherited Create;
FLat := ALat;
FLng := ALng
end;
// **************
constructor TMercatorProjection.Create;
begin
inherited Create;
pixelOrigin_ := TG_Point.Create( Round(MERCATOR_RANGE / 2), Round(MERCATOR_RANGE / 2));
pixelsPerLonDegree_ := MERCATOR_RANGE / 360;
pixelsPerLonRadian_ := MERCATOR_RANGE / (2 * PI);
end;
// Translate degrees to radians
function TMercatorProjection.degreesToRadians(deg: double): double;
begin
Result := deg * (PI / 180);
end;
// Translate radians to degrees
function TMercatorProjection.radiansToDegrees(rad: double): double;
begin
Result := rad / (PI / 180);
end;
// keep value insid defined bounds
function TMercatorProjection.bound(value: double; opt_min: double; opt_max: double): double;
begin
if Value < opt_min then Result := opt_min
else if Value > opt_max then Result := opt_max
else Result := Value;
end;
procedure TMercatorProjection.fromLatLngToPoint(latLng: TG_LatLng; var point: TG_Point);
var
siny: double;
begin
if Assigned(point) then
begin
point.x := Round(pixelOrigin_.x + latLng.lng * pixelsPerLonDegree_);
// NOTE(appleton): Truncating to 0.9999 effectively limits latitude to
// 89.189. This is about a third of a tile past the edge of the world tile.
siny := bound(sin(degreesToRadians(latLng.lat)), -0.9999, 0.9999);
point.y := Round(pixelOrigin_.y + 0.5 * ln((1 + siny) / (1 - siny)) * -pixelsPerLonRadian_);
end;
end;
procedure TMercatorProjection.fromPointToLatLng(point: TG_point; var latLng: TG_LatLng);
var
latRadians: double;
begin
if Assigned(latLng) then
begin
latLng.lng := (point.x - pixelOrigin_.x) / pixelsPerLonDegree_;
latRadians := (point.y - pixelOrigin_.y) / -pixelsPerLonRadian_;
latLng.lat := radiansToDegrees(2 * arctan(exp(latRadians)) - PI / 2);
end;
end;
//pixelCoordinate = worldCoordinate * pow(2,zoomLevel)
procedure TMercatorProjection.getCorners(center: TG_LatLng; zoom: integer; mapWidth: integer; mapHeight: integer;
var NELatLon: TG_LatLng; var SWLatLon: TG_LatLng);
var
scale: double;
centerPx, SWPoint, NEPoint: TG_Point;
begin
scale := power(2, zoom);
centerPx := TG_Point.Create(0, 0);
try
fromLatLngToPoint(center, centerPx);
SWPoint := TG_Point.Create(Round(centerPx.x-(mapWidth/2)/scale), Round(centerPx.y+(mapHeight/2)/scale));
NEPoint := TG_Point.Create(Round(centerPx.x+(mapWidth/2)/scale), Round(centerPx.y-(mapHeight/2)/scale));
try
fromPointToLatLng(SWPoint, SWLatLon);
fromPointToLatLng(NEPoint, NELatLon);
finally
SWPoint.Free;
NEPoint.Free;
end;
finally
centerPx.Free;
end;
end;
end.
Usage example:
with TMercatorProjection.Create do
try
CLatLon := TG_LatLng.Create(Latitude, Longitude);
SWLatLon := TG_LatLng.Create(0,0);
NELatLon := TG_LatLng.Create(0,0);
try
getCorners(CLatLon, Zoom,
MapWidth, MapHeight,
SWLatLon, NELatLon);
finally
ShowMessage('SWLat='+FloatToStr(SWLatLon.Lat)+' | SWLon='+FloatToStr(SWLatLon.Lng));
ShowMessage('NELat='+FloatToStr(NELatLon.Lat)+' | NELon='+FloatToStr(NELatLon.Lng));
SWLatLon.Free;
NELatLon.Free;
CLatLon.Free;
end;
finally
Free;
end;
For big zoom factors (>=8), where non-uniformity of map scale on y axis can be neglected, there is much easier method, where we just takes into accout 1/cos(latitude) correction for pixels/(degrees of latitude) resolution. Initial resolution for zoom=0 is 256 pixels per 360 degrees both for x and y at 0 lattitude.
def get_static_map_bounds(lat, lng, zoom, sx, sy):
# lat, lng - center
# sx, sy - map size in pixels
# 256 pixels - initial map size for zoom factor 0
sz = 256 * 2 ** zoom
#resolution in degrees per pixel
res_lat = cos(lat * pi / 180.) * 360. / sz
res_lng = 360./sz
d_lat = res_lat * sy / 2
d_lng = res_lng * sx / 2
return ((lat-d_lat, lng-d_lng), (lat+d_lat, lng+d_lng))
Related
I need to know if a location on the map is currenlt viewed by the user,
For example I have a point
{latititude:40,longtitude:32}
And the view of the map is made from
{latitude: 32,longtitude:44, latitudeDelta:0.003,longtitudeDelta:0.005}
When the map zooms in and out the only difference in data is the latitudeDelta and the longtitudeDelta
Eventually when I'll zoom more and more the point on the map will not be visible anymore
Can someone help me build this calculation it will be very helpfull
I can't give you a coded solution but you can:
get the diameter from the delta (from 2 angles of the map):
const diameter = Math.floor((location.latitudeDelta * 40008000) / 360)
Then calculate the distance between 2 positions:
function distance(lon1, lat1, lon2, lat2) {
const R = 6371 // Radius of the earth in km
const dLat = (lat2 - lat1).toRad() // Javascript functions in radians
const dLon = (lon2 - lon1).toRad()
const a = Math.sin(dLat / 2) * Math.sin(dLat / 2) +
Math.cos(lat1.toRad()) * Math.cos(lat2.toRad()) *
Math.sin(dLon / 2) * Math.sin(dLon / 2)
const c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a))
const d = R * c // Distance in km
return d
}
// add this if toRad is undefined
if (typeof (Number.prototype.toRad) === 'undefined') {
Number.prototype.toRad = function() {
return this * Math.PI / 180
}
}
Then with the diameter and the distance:
if ((diameter / 2) > distance) {
console.log('The location in in the map')
}
I've started to create a website where it's users are effectively tracked (they know they are being tracked). Users will walk a particular route (around Manchester, UK. to be more precise) of which there are 10 checkpoints. A checkpoint is a static position on the map. Using the Google Maps API I know that I can plot a position on a map i.e. a checkpoint. I am also storing the time at which a user reaches said checkpoint. Taking the distance between checkpoints I am then able to calculate their average speed using basic math.
Now what I would like to do is plot their estimated position based on their speed. The difficulty that I am having is plotting a new position x miles/meters (any unit) from the current position along the route.
Had it been a straight line, this would have been simple.
Is there a way to calculate a distance from the current position along the route?
Are there any restrictions on the number of points?
Are there specific ways of doing this that should be avoided?
To expand my example with an image:
Imagine that a user reached the first place marker at 07:00am and it's estimated they would reach the second place marker at 09:00am. The time now (for example) is 08:00am meaning that (estimated) the user should be about half way between the markers. I would then calculate the distance they have walked (again, estimated) and plot their position on the map "distance" away from the first place marker.
Hopefully I have explained the scenario clear enough for people to understand.
I'm relatively new to the Google maps API so any thoughts would be helpful. Other similar questions have been asked on SO but from what I can see, none have been answered or have requested as many details as I have.
Thanks in advance.
UPDATE: Having spent a lot of time trying to work it out I failed miserably. Here is what I know:
I should create the path using a PolyLine (I can do this, I have a list of lat/lng)
There is a JS extension called epoly.js but this isn't compatible with V3
Using spherical.interpolate wont work because it doesn't follow the path.
I used to do a lot of this stuff in a past life as a cartographer. Your polyline is made up of a succession of points (lat/long coordinates). Between each successive point you calculate the distance, adding it up as you go along until you get to the desired distance.
The real trick is calculating the distance between two lat/long points which are spherical coordinates (ie points on a curved surface). Since you are dealing with fairly small distances you could feasibly convert the lat/long coordinates to the local map grid system (which is flat). The distance between two points is then straight forward right angle pythagoras (sum of the squares and all that). Movable Type website has a lot of good (javascript) code on this here.
The second way would be to do the spherical distance calculation - not pretty but you can see it here
Personally I'd go the route of converting the coordinates to the local grid system which in the UK should be OSGB. Its the least contorted method.
Hope this helps
Edit:
I've assumed that you can extract your polyline coordinates using the google api. I havn't done this in version 3 of the api, but it should be straight forward. Also, the polyline coordinates should be fairly close together that you don't need to interpolate intermediate points - just grab the nearest polyline coordinate (saves you having to do a bearing and distance calculation).
Edit2 - With Code
I've had a go at putting some code together, but probably won't have time to finish it within your time limit (I do have a job). You should be able to get the jist. The coordinate conversion code is lifted from the movable type web site and the basic google maps stuff from one of google's examples. Basically it draws a polyline with mouse clicks, puts the lat/long of each mouse click in table field, converts the coordinate to OSGB and then to OS Grid (see here). After the first click it then calculates the distance between each subsequent point. Hope this gets you on the road.
<!DOCTYPE html>
<html>
<head>
<meta name="viewport" content="initial-scale=1.0, user-scalable=no" />
<style type="text/css">
html { height: 100% }
body { height: 100%; margin: 0; padding: 0 }
#map_canvas { height: 100% }
</style>
<script type="text/javascript"
src="http://maps.googleapis.com/maps/api/js?sensor=false">
</script>
<script src="Map.js" type="text/javascript"></script>
</head>
<body onload="initialize()" style="width:100%;height:100%">
<div style="margin-right:auto;margin-left:auto;margin-top:100px;width:900px;">
<div id="map_canvas" style="width:600px; height:500px;float:left;"></div>
<div style="float:right;">
<table>
<tr>
<td align="right">Latitude:</td>
<td><input id="txtLatitude" maxlength="11" type="text" class="inputField"/></td>
</tr>
<tr>
<td align="right">Longitude:</td>
<td><input id="txtLongitude" maxlength="11" type="text" class="inputField"/></td>
</tr>
<tr>
<td align="right">Eastings:</td>
<td><input id="txtEast" maxlength="11" type="text" class="inputField"/></td>
</tr>
<tr>
<td align="right">Northings:</td>
<td><input id="txtNorth" maxlength="11" type="text" class="inputField"/></td>
</tr>
<tr>
<td align="right">Distance:</td>
<td><input id="txtDistance" maxlength="11" type="text" class="inputField"/></td>
</tr>
<tr>
<td colspan=2 align="right">
</td>
</tr>
</table>
</div>
</div>
</body>
</html>
Map.js:
function initialize() {
var myOptions = {
center: new google.maps.LatLng(53.43057, -2.14727),
zoom: 18,
mapTypeId: google.maps.MapTypeId.ROADMAP
};
var map = new google.maps.Map(document.getElementById("map_canvas"), myOptions);
var tempIcon = new google.maps.MarkerImage(
"http://labs.google.com/ridefinder/images/mm_20_green.png",
new google.maps.Size(12, 20),
new google.maps.Size(6, 20)
);
var newShadow = new google.maps.MarkerImage(
"http://labs.google.com/ridefinder/images/mm_20_shadow.png",
new google.maps.Size(22, 20),
new google.maps.Point(13, 13)
);
var tempMarker = new google.maps.Marker();
tempMarker.setOptions({
icon: tempIcon,
shadow: newShadow,
draggable: true
});
var latlngs = new google.maps.MVCArray();
var displayPath = new google.maps.Polyline({
map: map,
strokeColor: "#FF0000",
strokeOpacity: 1.0,
strokeWeight: 2,
path: latlngs
});
var lastEast;
var lastNorth;
function showTempMarker(e) {
//Pythagorean distance calculates the length of the hypotenuse (the sloping side)
//of a right angle triangle. Plain (cartesian) coordinates are all right angle triangles.
//The length of the hypotenuse is always the distance between two coordinates.
//One side of the triangle is the difference in east coordinate and the other is
//the difference in north coordinates
function pythagorasDistance(E, N) {
if (lastEast) {
if (lastEast) {
//difference in east coordinates. We don't know what direction we are going so
//it could be a negative number - so just take the absolute value (ie - get rid of any minus sign)
var EastDistance = Math.abs(E - lastEast);
//difference in north coordinates
var NorthDistance = Math.abs(N - lastNorth);
//take the power
var EastPower = Math.pow(EastDistance, 2);
var NorthPower = Math.pow(NorthDistance, 2);
//add them together and take the square root
var pythagorasDistance = Math.sqrt(EastPower + NorthPower );
//round the answer to get rid of ridiculous decimal places (we're not measuring to the neares millimetre)
var result = Math.floor(pythagorasDistance);
document.getElementById('txtDistance').value = result;
}
}
}
function calcCatesian(degLat, degLng) {
var OSGBLL = LL.convertWGS84toOSGB36(new LatLon(degLat, degLng));
var EN = LL.LatLongToOSGrid(OSGBLL);
document.getElementById('txtEast').value = EN.east;
document.getElementById('txtNorth').value = EN.north;
pythagorasDistance(EN.east, EN.north);
lastEast = EN.east;
lastNorth = EN.north;
}
tempMarker.setPosition(e.latLng);
var lat = e.latLng.lat();
var lng = e.latLng.lng();
document.getElementById('txtLatitude').value = lat;
document.getElementById('txtLongitude').value = lng;
calcCatesian(lat, lng);
google.maps.event.addListener(tempMarker, "drag", function() {
document.getElementById('txtLatitude').value = tempMarker.getPosition().lat();
document.getElementById('txtLongitude').value = tempMarker.getPosition().lng();
calcCatesian(lat, lng);
});
tempMarker.setMap(map);
var newLocation = new google.maps.LatLng(lat, lng);
latlngs.push(newLocation);
displayPath.setPath(latlngs);
}
google.maps.event.addListener(map, "click", showTempMarker);
}
// ---- the following are duplicated from LatLong.html ---- //
/*
* construct a LatLon object: arguments in numeric degrees & metres
*
* note all LatLong methods expect & return numeric degrees (for lat/long & for bearings)
*/
function LatLon(lat, lon, height) {
if (arguments.length < 3)
height = 0;
this.lat = lat;
this.lon = lon;
this.height = height;
}
function setPrototypes() {
/*
* represent point {lat, lon} in standard representation
*/
LatLon.prototype.toString = function() {
return this.lat.toLat() + ', ' + this.lon.toLon();
}
// extend String object with method for parsing degrees or lat/long values to numeric degrees
//
// this is very flexible on formats, allowing signed decimal degrees, or deg-min-sec suffixed by
// compass direction (NSEW). A variety of separators are accepted (eg 3º 37' 09"W) or fixed-width
// format without separators (eg 0033709W). Seconds and minutes may be omitted. (Minimal validation
// is done).
String.prototype.parseDeg = function() {
if (!isNaN(this))
return Number(this); // signed decimal degrees without NSEW
var degLL = this.replace(/^-/, '').replace(/[NSEW]/i, ''); // strip off any sign or compass dir'n
var dms = degLL.split(/[^0-9.]+/); // split out separate d/m/s
for (var i in dms)
if (dms[i] == '')
dms.splice(i, 1);
// remove empty elements (see note below)
switch (dms.length) { // convert to decimal degrees...
case 3:
// interpret 3-part result as d/m/s
var deg = dms[0] / 1 + dms[1] / 60 + dms[2] / 3600;
break;
case 2:
// interpret 2-part result as d/m
var deg = dms[0] / 1 + dms[1] / 60;
break;
case 1:
// decimal or non-separated dddmmss
if (/[NS]/i.test(this))
degLL = '0' + degLL; // - normalise N/S to 3-digit degrees
var deg = dms[0].slice(0, 3) / 1 + dms[0].slice(3, 5) / 60 + dms[0].slice(5) / 3600;
break;
default:
return NaN;
}
if (/^-/.test(this) || /[WS]/i.test(this))
deg = -deg; // take '-', west and south as -ve
return deg;
}
// note: whitespace at start/end will split() into empty elements (except in IE)
// extend Number object with methods for converting degrees/radians
Number.prototype.toRad = function() { // convert degrees to radians
return this * Math.PI / 180;
}
Number.prototype.toDeg = function() { // convert radians to degrees (signed)
return this * 180 / Math.PI;
}
// extend Number object with methods for presenting bearings & lat/longs
Number.prototype.toDMS = function(dp) { // convert numeric degrees to deg/min/sec
if (arguments.length < 1)
dp = 0; // if no decimal places argument, round to int seconds
var d = Math.abs(this); // (unsigned result ready for appending compass dir'n)
var deg = Math.floor(d);
var min = Math.floor((d - deg) * 60);
var sec = ((d - deg - min / 60) * 3600).toFixed(dp);
// fix any nonsensical rounding-up
if (sec == 60) {
sec = (0).toFixed(dp);
min++;
}
if (min == 60) {
min = 0;
deg++;
}
if (deg == 360)
deg = 0;
// add leading zeros if required
if (deg < 100)
deg = '0' + deg;
if (deg < 10)
deg = '0' + deg;
if (min < 10)
min = '0' + min;
if (sec < 10)
sec = '0' + sec;
return deg + '\u00B0' + min + '\u2032' + sec + '\u2033';
}
Number.prototype.toLat = function(dp) { // convert numeric degrees to deg/min/sec latitude
return this.toDMS(dp).slice(1) + (this < 0 ? 'S' : 'N'); // knock off initial '0' for lat!
}
Number.prototype.toLon = function(dp) { // convert numeric degrees to deg/min/sec longitude
return this.toDMS(dp) + (this > 0 ? 'E' : 'W');
}
/*
* extend Number object with methods for converting degrees/radians
*/
Number.prototype.toRad = function() { // convert degrees to radians
return this * Math.PI / 180;
}
Number.prototype.toDeg = function() { // convert radians to degrees (signed)
return this * 180 / Math.PI;
}
/*
* pad a number with sufficient leading zeros to make it w chars wide
*/
Number.prototype.padLZ = function(w) {
var n = this.toString();
for (var i = 0; i < w - n.length; i++)
n = '0' + n;
return n;
}
};
setPrototypes();
LL = function() {
// ellipse parameters
var e = {
WGS84: {
a: 6378137,
b: 6356752.3142,
f: 1 / 298.257223563
},
Airy1830: {
a: 6377563.396,
b: 6356256.910,
f: 1 / 299.3249646
}
};
// helmert transform parameters
var h = {
WGS84toOSGB36: {
tx: -446.448,
ty: 125.157,
tz: -542.060, // m
rx: -0.1502,
ry: -0.2470,
rz: -0.8421, // sec
s: 20.4894
}, // ppm
OSGB36toWGS84: {
tx: 446.448,
ty: -125.157,
tz: 542.060,
rx: 0.1502,
ry: 0.2470,
rz: 0.8421,
s: -20.4894
}
};
return {
convertOSGB36toWGS84: function(p1) {
var p2 = this.convert(p1, e.Airy1830, h.OSGB36toWGS84, e.WGS84);
return p2;
},
convertWGS84toOSGB36: function(p1) {
var p2 = this.convert(p1, e.WGS84, h.WGS84toOSGB36, e.Airy1830);
return p2;
},
convert: function(p1, e1, t, e2) {
// -- convert polar to cartesian coordinates (using ellipse 1)
p1.lat = p1.lat.toRad();
p1.lon = p1.lon.toRad();
var a = e1.a, b = e1.b;
var sinPhi = Math.sin(p1.lat), cosPhi = Math.cos(p1.lat);
var sinLambda = Math.sin(p1.lon), cosLambda = Math.cos(p1.lon);
var H = p1.height;
var eSq = (a * a - b * b) / (a * a);
var nu = a / Math.sqrt(1 - eSq * sinPhi * sinPhi);
var x1 = (nu + H) * cosPhi * cosLambda;
var y1 = (nu + H) * cosPhi * sinLambda;
var z1 = ((1 - eSq) * nu + H) * sinPhi;
// -- apply helmert transform using appropriate params
var tx = t.tx, ty = t.ty, tz = t.tz;
var rx = t.rx / 3600 * Math.PI / 180; // normalise seconds to radians
var ry = t.ry / 3600 * Math.PI / 180;
var rz = t.rz / 3600 * Math.PI / 180;
var s1 = t.s / 1e6 + 1; // normalise ppm to (s+1)
// apply transform
var x2 = tx + x1 * s1 - y1 * rz + z1 * ry;
var y2 = ty + x1 * rz + y1 * s1 - z1 * rx;
var z2 = tz - x1 * ry + y1 * rx + z1 * s1;
// -- convert cartesian to polar coordinates (using ellipse 2)
a = e2.a, b = e2.b;
var precision = 4 / a; // results accurate to around 4 metres
eSq = (a * a - b * b) / (a * a);
var p = Math.sqrt(x2 * x2 + y2 * y2);
var phi = Math.atan2(z2, p * (1 - eSq)), phiP = 2 * Math.PI;
while (Math.abs(phi - phiP) > precision) {
nu = a / Math.sqrt(1 - eSq * Math.sin(phi) * Math.sin(phi));
phiP = phi;
phi = Math.atan2(z2 + eSq * nu * Math.sin(phi), p);
}
var lambda = Math.atan2(y2, x2);
H = p / Math.cos(phi) - nu;
return new LatLon(phi.toDeg(), lambda.toDeg(), H);
},
/*
* convert numeric grid reference (in metres) to standard-form grid ref
*/
gridrefNumToLet: function(e, n, digits) {
// get the 100km-grid indices
var e100k = Math.floor(e / 100000), n100k = Math.floor(n / 100000);
if (e100k < 0 || e100k > 6 || n100k < 0 || n100k > 12)
return '';
// translate those into numeric equivalents of the grid letters
var l1 = (19 - n100k) - (19 - n100k) % 5 + Math.floor((e100k + 10) / 5);
var l2 = (19 - n100k) * 5 % 25 + e100k % 5;
// compensate for skipped 'I' and build grid letter-pairs
if (l1 > 7)
l1++;
if (l2 > 7)
l2++;
var letPair = String.fromCharCode(l1 + 'A'.charCodeAt(0), l2 + 'A'.charCodeAt(0));
// strip 100km-grid indices from easting & northing, and reduce precision
e = Math.floor((e % 100000) / Math.pow(10, 5 - digits / 2));
n = Math.floor((n % 100000) / Math.pow(10, 5 - digits / 2));
var gridRef = letPair + e.padLZ(digits / 2) + n.padLZ(digits / 2);
return gridRef;
},
LatLongToOSGrid: function(p) {
var lat = p.lat.toRad(), lon = p.lon.toRad();
var a = 6377563.396, b = 6356256.910; // Airy 1830 major & minor semi-axes
var F0 = 0.9996012717; // NatGrid scale factor on central meridian
var lat0 = (49).toRad(), lon0 = (-2).toRad(); // NatGrid true origin
var N0 = -100000, E0 = 400000; // northing & easting of true origin, metres
var e2 = 1 - (b * b) / (a * a); // eccentricity squared
var n = (a - b) / (a + b), n2 = n * n, n3 = n * n * n;
var cosLat = Math.cos(lat), sinLat = Math.sin(lat);
var nu = a * F0 / Math.sqrt(1 - e2 * sinLat * sinLat); // transverse radius of curvature
var rho = a * F0 * (1 - e2) / Math.pow(1 - e2 * sinLat * sinLat, 1.5); // meridional radius of curvature
var eta2 = nu / rho - 1;
var Ma = (1 + n + (5 / 4) * n2 + (5 / 4) * n3) * (lat - lat0);
var Mb = (3 * n + 3 * n * n + (21 / 8) * n3) * Math.sin(lat - lat0) * Math.cos(lat + lat0);
var Mc = ((15 / 8) * n2 + (15 / 8) * n3) * Math.sin(2 * (lat - lat0)) * Math.cos(2 * (lat + lat0));
var Md = (35 / 24) * n3 * Math.sin(3 * (lat - lat0)) * Math.cos(3 * (lat + lat0));
var M = b * F0 * (Ma - Mb + Mc - Md); // meridional arc
var cos3lat = cosLat * cosLat * cosLat;
var cos5lat = cos3lat * cosLat * cosLat;
var tan2lat = Math.tan(lat) * Math.tan(lat);
var tan4lat = tan2lat * tan2lat;
var I = M + N0;
var II = (nu / 2) * sinLat * cosLat;
var III = (nu / 24) * sinLat * cos3lat * (5 - tan2lat + 9 * eta2);
var IIIA = (nu / 720) * sinLat * cos5lat * (61 - 58 * tan2lat + tan4lat);
var IV = nu * cosLat;
var V = (nu / 6) * cos3lat * (nu / rho - tan2lat);
var VI = (nu / 120) * cos5lat * (5 - 18 * tan2lat + tan4lat + 14 * eta2 - 58 * tan2lat * eta2);
var dLon = lon - lon0;
var dLon2 = dLon * dLon, dLon3 = dLon2 * dLon, dLon4 = dLon3 * dLon, dLon5 = dLon4 * dLon, dLon6 = dLon5 * dLon;
var N = I + II * dLon2 + III * dLon4 + IIIA * dLon6;
var E = E0 + IV * dLon + V * dLon3 + VI * dLon5;
E = Math.floor(E * 100) / 100;
N = Math.floor(N * 100) / 100;
//return this.gridrefNumToLet(E, N, 8);
return { east: E, north: N }
;
}
}
} ();
I think you are looking for something similar to this function, which returns a point a certain percentage along a given line. Unfortuntaely I'm not aware offhand of a javascript port of this function, but it's probably worth a look.
Meanwhile here's a quick concept for a hack that may give you enough detail for your needs:
Start with your polyline (for simplicity's sake let's assume you have just a single path, which is a series of LatLngs)
When you want to estimate where the person is, take their percentage along the path as determined by the time (for example 8am they are 50% along)
Now for each LatLng in your path, calculate it's fractional distance along the total length of the path by adding the distances between LatLngs (you can use the computeLength for the path, and computeDistanceBetween for each LatLng)
As soon as you get to a fraction >50% along (in this case), you know the person is inbetween this LatLng and the previous one. You can then calculate exactly how far along as well to place the point exactly if you wish, or just skip this step if it's a pretty short segment and put their marker at one of these LatLngs.
The above is the general concept, but of course you should optimize by precomputing the percentage distances for each LatLng just once for each Path and storing that in a separate object, and keep track of your last index in the path so you don't start from the beginning next time you calculate their distance along, etc.
Hope this helps.
I think you pretty much got the answer already, except for one little detail that I haven't seen anybody mention explicitly: you need to use the encoded polyline from the steps to get to the point where you will be interpolating between two points that are close enough so that the straight line between them is a good approximation to the shape of the route.
Let's see an example:
Driving directions from Madrid to Toledo:
http://maps.googleapis.com/maps/api/directions/json?origin=Toledo&destination=Madrid®ion=es&sensor=false
The median point (half way through the whole route) would be somewhere in the biggest step which is nearly 50 km long:
{
"distance" : {
"text" : "49.7 km",
"value" : 49697
},
"duration" : {
"text" : "26 mins",
"value" : 1570
},
"end_location" : {
"lat" : 40.26681000000001,
"lng" : -3.888580
},
"html_instructions" : "Continue onto \u003cb\u003eAP-41\u003c/b\u003e\u003cdiv style=\"font-size:0.9em\"\u003eToll road\u003c/div\u003e",
"polyline" : {
"points" : "kdtrFj`~VEkA[oLEy#Cs#KeAM_BOgBy#eGs#iFWgBS{AQ{AQcBMuAM}BKcBGiCM_EGaFKgEKeDOqC[yFWwCIi#Is#i#_EaAsFy#aEeAcEsAqEuAeE]w#k#qAeAcCm#sA}#cBgB{CaBgCiEyFuB}BsBuBaDwCa#]_CsBmD_Di#g#aAaAs#s#{#aAU[q#w#s#{#wE{Ge#s#_#o#a#q#o#kAmAaCaAqBeAcCw#kBy#yBw#_Cg#aB}AkEoBgFyDwIcBwDa#iAcBwCgAcBsAiBy#uAeCiDqBeCaB_BuA_BiDeC{#o#u#k#cD{B}#g#y#_#k#]cD_BkD{AwBu#cA]eAYsD_AuE_AqBY{Du#_BW_AQm#K_AOiAWuAa#qA_#mA_#aBi#MGOGMGy#[OI_Bw#uBkAkDsBaAg#oEiCg#YeEcC}As#m#WqCmAmEeBuAe#mDeAqCq#qCm#iB]eBY}BYeCYi#C{#IgBMwCMmAEmAC{A#mB?wBFsBBiBHeAJcBNgBNcBRcC\\qCd#sBb#eAXeBd#uBn#{Bp#uAd#}B~#gD|AmCrA{#j#}Az#kDvB{AbAqA|#}AnAaAx#aAv#}AtAaA`AwClD{HzImH~IiF|F{#~#o#v#qAhAsAhAqA`AyAbA{A~#m#Xw#h#gCnAiBz#uAt#wAh#}#XwCz#qBd#qCf#gBXkBTw#FaCTmDPsADwDJgCFoFXwDXoDb#gCd#wB`#gCh#_D~#qC~#gC~#wChAmDxAaC|#sCbAgEzAuGbBaB`#cFdAo#NoAXiC^cD^oDXmEToBJkABA?Q#_##yDBwBAoB?wBEm#A_CKO?_EUcD[eEe#uAQq#I]GqDs#e#Ii#K_#IOEgAWk#MsBi#mBg#WIc#MkEwA{Am#yB}#yDcB_CgAsAs#eB}#aBaAiD{ByCqBkA}#mA}#uAiAwCcCyAoAmEiE{#aAgAyA{#cAmAuAaBsBkAyAgBcCwAoBwAwByCyEyBmD{BsDgCaEuA{Co#eAuC_Fs#iA{#iAo#_A{A}BkGmHmAwAeBaBcBeBcHsGmCkCyCyCm#m#m#m#_A_AkDaDkCiCkDgD}#y#iE_FcC}CkBkCw#gAs#cAcC{D_BmCe#}#}AuCsAkCiCqFkAgCe#kAgAeCw#mBuAaDWg#g#iAEE{AqCq#kA_#k#oCwDuAeBoAqAUQ_#YMOm#k#e#g#_#]u#m#k#a#i#_#YOSOe#[k#_#w#c#a#Ok#WyAo#y#[eBm#}Ac#{Bk#kASwBS}AMyBO}BGuGJsAJmCRuDn#iCn#}C`AwBx#kB|#{BfAmBfAkCdBaCzA_BpA_BlAuAnAeCdCuD`EgBzBgClDyBrD{AtCy#bB_#b#Wl#c#`AWr#KVSd#MXIPGPSd#MZQb#MZ_#bAm#dBQd#[`A_#jAGRIVGPGVIVSt#k#xBe#jBKd#U`As#nDKb#Q`AgAtHADM~ACNK|#SpBQhBKnBKxACv#OhDO~EGdFAhA#|CC~B?rA#vB#hB#dD#vBAbCCvCEzBGpBEpAEpAKrBI~#Ej#Et#WxCa#vDYrBq#bEQfAUnAy#vD}BtJUx#K^wBfGwCdHqBxD_B`CsBbDwCnEgCrCuCzCyBpBiCzBmBvAaC|AuAv#eAj#OHuAp#}#^oBz#eExAgBb#uFpAaC`#mDb#iCRmADaDFy#B}E?aEQaJcAuB]uA[uBc#kDu#eD{#{Cs#iDu#wBe#eEo#{BQwDYiEMkBEaA?aA?aB?_B#aBDsBJwDT{Ed#o#JcALi#JcBVuBb#sA\\eAV{Ct#aA\\cBh#_Bh#mAb#sCpAwDhB_CpA}BvAg#\\mAr#aBjAqBzAgBxAeBzAoBlB_C~BgAhAUV[`#uCjD_BvBQVGDw#fAiAdBeAdBuC`Fe#|#wCbGU^]r#]r#oAvCeApCQZKXo#vBu#|B}#zCoAjEg#vBc#~AOt#k#~Bu#jD}#tDc#zAW`AWv#Ux#[bAw#xBuAhD{#jByCvFcClDwBvCkCrCgCdCsBzAgBnAkBjAkBbAmAj#gAf#mDjAsBl#sBf#uBb#oBXaBLiBNaADgABuABkBCgEUuCU}Ck#_Cg#kCu#yB{#{BaAqBaA}#i#kAq#OIe#[g#_#a#WmAaAeAy#iAeA}#_AmAsAu#w#{#gA_#e#o#cAk#_Ay#sAYm#_#m#_#u#]q#u#cBi#eA[y#Se#g#iAYs#_#oAMi#[aAa#uA_#wAS}#a#cB]wAWqAI]CKgAyDu#yCo#eCgAmDu#cCmAoDmBwEgAaCa#_AcByCqDwGiBkCw#iAgBaCkAoAiC{CkBiBuAsAoBcBeEaD}BaBs#c#gCyAKEoBgAuAk#eBy#oAe#uCcAgBo#mD_AkCk#kAUsASgAQeAIm#ImCW_E[_FWwCSkBMuAM[E{#IGAmBUmCc#}#QcAUsA_#cAWgBi#w#UwAk#a#MmAi#eAe#yBiAk#[SMKEw#g#k#_#w#e#aC_Bc#]yBgBeCmB}BmB}BsB_BoAw#o#s#g#oDiCuA{#_BcAgAq#uBsAaAc#{#_#y#_#sAm#yD}AeDgAsDiAeCeAaCy#iCgAiBcAeAc#c#OyE{A{Ag#y#YaBm#{Aq#gAm#i#][YMMYWaGwGi#y#{A{B_A{Aw#iAs#iA_A}AcAaBsAiBeBkBoAiAaBsA{AcAoAq#iB}#qBu#wBk#cBa#qAW}#I}CSwEBiDVcBR_BXiCr#gBp#wBbAkAp#qA|#y#l#wCjC{#~#gArAmCzDiAnBm#tAu#jBq#pBmAvDwAnFw#bCELq#tBw#pBgAdCS\\qCnF_#f#yBtC{AhBqAvAkBhB{ArAyAhAg#Ze#Z{BrAiBz#SHy#^OFs#X_AZ_Bd#WJaDr#}B\\eBPoBNq#F_##iC#gACu#Ai#Ey#IQC_B[}Bo#_#Ks#S"
},
"start_location" : {
"lat" : 39.92150,
"lng" : -3.927260
},
"travel_mode" : "DRIVING"
},
I'm afraid this polyline is too long (2856 characters) to display it directly in the Static Maps API, but that's not necessary, it'd just be a nice way to show the polyline right here. Anyway, you can use the Interactive Polyline Encoder Utility to paste this encoded polyline (after replacing \\ with \) to see it.
Now, let's imagine you need to find the point in this route that is exactly 20 km. from the start of this step. That is, the point between start_location and end_location that is 20,000 meters from start_location, along the route defined by the above polyline.
In your app, you'd use the Encoding Methods in the Geometry Library (which you need to load explicitly) to decode this polyline into the whole array of LatLng points. You'd then use the computeDistanceBetween between each two adjacent points to figure out which one is the first LatLng point (Y) in that polyline that is more than 20,000 from start_location. Then you take that point plus the previous one (X) and do the straight-line interpolation between X and Y. At this point, you can count on the straight line between these two points to be a reasonable approximation to the shape of the route.
Mind you, this is a fairly detailed calculation that may turn up too expensive. If you hit performance issues due to the big size of the polyline, you can simplify it by dropping part of the points. Doing this simplification smartly may be, again, expensive though, so I'd keep it simple ;)
I would say it's doable. :-) This is how I visualize it, but I haven't tested any of it.
First define a PolyLine based on the "guessed route" which the users are supposed to take. Store that in a local variable in your js. It will be handy to have lots of points, to make the estimated point better.
Then set up an interval (window.setInterval) to check for updates in users positions, say every 30 seconds. If the position is newer than the interval - display the known position and draw a solid line from the last known position, creating a line of known data. (setPath)
When no new data is present, do a simple velocity calculation using the latest few known points.
Using the velocity and the timeframe calculate an estimated travel distance.
Using the calculated distance, load your estimated route object and "walk" point by point in the "guessed route" until the pseudo walked distance is almost equal to your estimate. Then return the point where you have reached the right distance.
Draw a dotted line from the last known location to the guessed one.
Good luck!
PS.
A PolyLine is a line object consisting of many paths and waypoints
Calculate lengths between points using geometry spherical namespaces function "computeLength"
This site: http://www.gmap-pedometer.com/ may be of interest, as it lets the user draw routes, and adds mile or km markers along the route, so it must be doing a similar calculation to the one you require.
I have a Google Maps Circle drawn on v3 api. When the user has plotted there circle (or polygon if they choose), they can save the data to the server. If the user has picked a radial search, the Centre coordinates and the radius in feet is stored to the database. This means when the user reloads his search, it can pull through the circle again (like below).
I'm having 1 problem, however, which is when the user selects what search they would like to use. It loads the polygon fine, if they drew a polygon, and if it's a circle it pulls through the marker on the center. However what I need is a function in static maps to draw a circle.
A bit late in the game, but nothing I found solved my issue (serverside php only, no javascript).
I ended up getting there in the end and have detailed my method here: http://jomacinc.com/map-radius/ and the short version is below.
This PHP function will return an encoded polyline string of lat/lng points in a circle around the specified point, and at the specified radius. The function requires Gabriel Svennerberg’s PHP polyline encoding class available here (http://www.svennerberg.com/examples/polylines/PolylineEncoder.php.txt).
function GMapCircle($Lat,$Lng,$Rad,$Detail=8){
$R = 6371;
$pi = pi();
$Lat = ($Lat * $pi) / 180;
$Lng = ($Lng * $pi) / 180;
$d = $Rad / $R;
$points = array();
$i = 0;
for($i = 0; $i <= 360; $i+=$Detail):
$brng = $i * $pi / 180;
$pLat = asin(sin($Lat)*cos($d) + cos($Lat)*sin($d)*cos($brng));
$pLng = (($Lng + atan2(sin($brng)*sin($d)*cos($Lat), cos($d)-sin($Lat)*sin($pLat))) * 180) / $pi;
$pLat = ($pLat * 180) /$pi;
$points[] = array($pLat,$pLng);
endfor;
require_once('PolylineEncoder.php');
$PolyEnc = new PolylineEncoder($points);
$EncString = $PolyEnc->dpEncode();
return $EncString['Points'];
}
You can now the use the above function to create a static map.
/* set some options */
$MapLat = '-42.88188'; // latitude for map and circle center
$MapLng = '147.32427'; // longitude as above
$MapRadius = 100; // the radius of our circle (in Kilometres)
$MapFill = 'E85F0E'; // fill colour of our circle
$MapBorder = '91A93A'; // border colour of our circle
$MapWidth = 640; // map image width (max 640px)
$MapHeight = 480; // map image height (max 640px)
/* create our encoded polyline string */
$EncString = GMapCircle($MapLat,$MapLng, $MapRadius);
/* put together the static map URL */
$MapAPI = 'http://maps.google.com.au/maps/api/staticmap?';
$MapURL = $MapAPI.'center='.$MapLat.','.$MapLng.'&size='.$MapWidth.'x'.$MapHeight.'&maptype=roadmap&path=fillcolor:0x'.$MapFill.'33%7Ccolor:0x'.$MapBorder.'00%7Cenc:'.$EncString.'&sensor=false';
/* output an image tag with our map as the source */
echo '<img src="'.$MapURL.'" />'
function GMapCircle(lat,lng,rad,detail=8){
var uri = 'https://maps.googleapis.com/maps/api/staticmap?';
var staticMapSrc = 'center=' + lat + ',' + lng;
staticMapSrc += '&size=100x100';
staticMapSrc += '&path=color:0xff0000ff:weight:1';
var r = 6371;
var pi = Math.PI;
var _lat = (lat * pi) / 180;
var _lng = (lng * pi) / 180;
var d = (rad/1000) / r;
var i = 0;
for(i = 0; i <= 360; i+=detail) {
var brng = i * pi / 180;
var pLat = Math.asin(Math.sin(_lat) * Math.cos(d) + Math.cos(_lat) * Math.sin(d) * Math.cos(brng));
var pLng = ((_lng + Math.atan2(Math.sin(brng) * Math.sin(d) * Math.cos(_lat), Math.cos(d) - Math.sin(_lat) * Math.sin(pLat))) * 180) / pi;
pLat = (pLat * 180) / pi;
staticMapSrc += "|" + pLat + "," + pLng;
}
return uri + encodeURI(staticMapSrc);}
Javascript version
Based on the answer from Jomac, Here is a Java/Android version of the same code.
It uses the PolyUtil class from Google Maps Android API Utility Library to encode the path.
import android.location.Location;
import com.google.android.gms.maps.model.LatLng;
import com.google.maps.android.PolyUtil;
import java.util.ArrayList;
public class GoogleStaticMapsAPIServices
{
private static final double EARTH_RADIUS_KM = 6371;
private static String GOOGLE_STATIC_MAPS_API_KEY = "XXXXXXXXXXXXX";
public static String getStaticMapURL(Location location, int radiusMeters)
{
String pathString = "";
if (radiusMeters > 0)
{
// Add radius path
ArrayList<LatLng> circlePoints = getCircleAsPolyline(location, radiusMeters);
if (circlePoints.size() > 0)
{
String encodedPathLocations = PolyUtil.encode(circlePoints);
pathString = "&path=color:0x0000ffff%7Cweight:1%7Cfillcolor:0x0000ff80%7Cenc:" + encodedPathLocations;
}
}
String staticMapURL = "https://maps.googleapis.com/maps/api/staticmap?size=640x320&markers=color:red%7C" +
location.getLatitude() + "," + location.getLongitude() +
pathString +
"&key=" + GOOGLE_STATIC_MAPS_API_KEY;
return staticMapURL;
}
private static ArrayList<LatLng> getCircleAsPolyline(Location center, int radiusMeters)
{
ArrayList<LatLng> path = new ArrayList<>();
double latitudeRadians = center.getLatitude() * Math.PI / 180.0;
double longitudeRadians = center.getLongitude() * Math.PI / 180.0;
double radiusRadians = radiusMeters / 1000.0 / EARTH_RADIUS_KM;
double calcLatPrefix = Math.sin(latitudeRadians) * Math.cos(radiusRadians);
double calcLatSuffix = Math.cos(latitudeRadians) * Math.sin(radiusRadians);
for (int angle = 0; angle < 361; angle += 10)
{
double angleRadians = angle * Math.PI / 180.0;
double latitude = Math.asin(calcLatPrefix + calcLatSuffix * Math.cos(angleRadians));
double longitude = ((longitudeRadians + Math.atan2(Math.sin(angleRadians) * Math.sin(radiusRadians) * Math.cos(latitudeRadians), Math.cos(radiusRadians) - Math.sin(latitudeRadians) * Math.sin(latitude))) * 180) / Math.PI;
latitude = latitude * 180.0 / Math.PI;
path.add(new LatLng(latitude, longitude));
}
return path;
}
}
I think it is not possible to draw a circle on a static Google map. You would need to approximate the circle by a polyline (best in encoded format). This has already been mentioned in Stackoverflow and it is demonstrated e.g. by Free Map Tools.
Sharing my C# version
private string GMapCircle(double lat, double lng, double rad, int detail = 8)
{
const string uri = "https://maps.googleapis.com/maps/api/staticmap?";
var staticMapSrc = "center=" + lat + "," + lng;
staticMapSrc += "&zoom=16";
staticMapSrc += "&maptype=satellite";
staticMapSrc += "&key=[YOURKEYHERE]";
staticMapSrc += "&size=640x426";
staticMapSrc += "&path=color:0xff0000ff:weight:1";
const int r = 6371;
const double pi = Math.PI;
var latAux = (lat * pi) / 180;
var longAux = (lng * pi) / 180;
var d = (rad / 1000) / r;
var i = 0;
if (rad > 0)
{
for (i = 0; i <= 360; i += detail)
{
var brng = i * pi / 180;
var pLat = Math.Asin(Math.Sin(latAux) * Math.Cos(d) + Math.Cos(latAux) * Math.Sin(d) * Math.Cos(brng));
var pLng = ((longAux + Math.Atan2(Math.Sin(brng) * Math.Sin(d) * Math.Cos(latAux), Math.Cos(d) - Math.Sin(latAux) * Math.Sin(pLat))) * 180) / pi;
pLat = (pLat * 180) / pi;
staticMapSrc += "|" + pLat + "," + pLng;
}
}
else
{
//TODO - Add marker
}
return uri + staticMapSrc;
}
This solution uses the significantly more versatile Canvas API to draw over the map image . All code is in Typescript, so simply remove type declarations if you're using Javascript.
ADVANGES OF USING CANVAS:
Its easier to draw shapes on.
Those shapes can also be revised without re-requesting the map image from Google.
The drawing 'layer' can be serialized independently of the underlying map image.
USAGE:
// DEFINE BASIC VARIABLES
const latitude: -34.3566871,
const longitude: 18.4967666
const mapZoom = 12;
const imageWidth: 100;
const imageHeight: 100;
// INVOKE UTILITY FUNCTION
savePlaceImage({
// SET BASIC PROPS
latitude,
longitude,
mapZoom,
imageWidth,
imageHeight,
fileName: 'Cape Point',
// DRAW IMAGE USING CANVAS API
draw: ctx => {
// draw location as dot
ctx.fillStyle = '#FF3366';
ctx.beginPath();
ctx.arc(imageWidth / 2, imageHeight / 2, 10, 0, 2 * Math.PI);
ctx.fill();
// draw circle around location with 1 kilometer radius
ctx.strokeStyle = '#0000FF';
ctx.beginPath();
ctx.arc(imageWidth / 2, imageHeight / 2, pixelsPerMeter(latitude) * 1000, 0, 2 * Math.PI);
ctx.stroke();
}
})
UTILITIES:
function savePlaceImage(
config: {
latitude: number,
longitude: number,
mapZoom: number,
imageWidth: number,
imageHeight: number,
fileName: string,
draw: (ctx: CanvasRenderingContext2D) => void,
},
) {
// DOWNLOAD MAP IMAGE FROM GOOGLE'S STATIC MAPS API AS A BLOB
return from(axios.get<Blob>(`https://maps.googleapis.com/maps/api/staticmap`, {
params: {
key: GOOGLE_MAPS_API_KEY,
size: `${config.imageWidth}x${config.imageHeight}`,
zoom: `${config.mapZoom}`,
center: `${config.latitude},${config.longitude}`,
style: 'feature:all|element:labels|visibility:off',
},
responseType: 'blob',
// CONVERT BLOB TO BASE64 ENCODED STRING
}).then(response => {
const reader = new FileReader();
reader.readAsDataURL(response.data);
return new Promise<string>(resolve => reader.onloadend = () => resolve(reader.result as string));
// CREATE HTML IMG ELEMENT, SET IT'S SRC TO MAP IMAGE, AND WAIT FOR IT TO LOAD
}).then(response => {
const image = document.createElement('img');
image.src = response;
return new Promise<HTMLImageElement>(resolve => image.onload = () => resolve(image));
// CREATE HTML CANVAS ELEMENT, THEN DRAW ON TOP OF CANVAS USING CANVAS API, THEN CONVERT TO BLOB
}).then(image => {
const canvas = document.createElement('canvas');
canvas.width = config.imageWidth;
canvas.height = config.imageHeight;
const ctx = canvas.getContext('2d');
ctx.drawImage(image, 0, 0);
config.draw(ctx);
return new Promise<Blob>(resolve => canvas.toBlob(blob => resolve(blob)));
// ATTACH BLOB TO HTML FORM WHICH CONVERTS IT TO A FILE TO BE POSTED, THEN SEND FILE TO SERVER
}).then(blob => {
const form = new FormData();
form.append('blob', blob, `${config.fileName}.png`);
const file = form.get('blob') as File;
return axios.post<{ file }>('https://www.my-api.com/save-image', form);
}));
}
function pixelsPerMeter(latitude: number) {
const radiusOfEarthInKilometers = 6371;
return Math.cos(latitude * Math.PI / 180) * 2 * Math.PI * radiusOfEarthInKilometers / (256 * Math.pow(2, 12));
}
python version, polyline library used for encoding the polygon
import math, polyline
def g_map_circle(lat,lng,radius,detail=8):
points = []
r = 6371
pi = math.pi
_lat = (lat * pi) /180
_lng = (lng * pi) /180
d = radius / r
i = 0
while i <= 360:
i = i + detail
brng = i * pi /180
p_lat = math.asin(math.sin(_lat) * math.cos(d) + math.cos(_lat) * math.sin(d) * math.cos(brng));
p_lng = (_lng + math.atan2(math.sin(brng) * math.sin(d) * math.cos(_lat), math.cos(d) - math.sin(_lat) * math.sin(p_lat))) * 180 / pi
p_lat = (p_lat * 180) /pi
points.append((p_lat,p_lng))
return polyline.encode(points)
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());
}
I have a point (X,Y) and I want to create a square , Google maps LatLngBounds object so to make geocode requests bias only into this LatLngBound region.
How can I create such a LatLngBounds square with center the given point? I have to find the NE and SW point. But how can I find it given a distance d and a point (x,y)?
Thanks
You can also getBounds from a radius defined as a circle and leave the trig to google.
new google.maps.Circle({center: latLng, radius: radius}).getBounds();
well that's very complicated. for a rough box try this:
if (typeof(Number.prototype.toRad) === "undefined") {
Number.prototype.toRad = function() {
return this * Math.PI / 180;
}
}
if (typeof(Number.prototype.toDeg) === "undefined") {
Number.prototype.toDeg = function() {
return this * 180 / Math.PI;
}
}
var dest = function(lat,lng,brng, dist) {
this._radius = 6371;
dist = typeof(dist) == 'number' ? dist : typeof(dist) == 'string' && dist.trim() != '' ? +dist : NaN;
dist = dist / this._radius;
brng = brng.toRad();
var lat1 = lat.toRad(),
lon1 = lng.toRad();
var lat2 = Math.asin(Math.sin(lat1) * Math.cos(dist) + Math.cos(lat1) * Math.sin(dist) * Math.cos(brng));
var lon2 = lon1 + Math.atan2(Math.sin(brng) * Math.sin(dist) * Math.cos(lat1), Math.cos(dist) - Math.sin(lat1) * Math.sin(lat2));
lon2 = (lon2 + 3 * Math.PI) % (2 * Math.PI) - Math.PI;
return (lat2.toDeg() + ' ' + lon2.toDeg());
}
var northEastCorner = dest(centreLAT,centreLNG,45,10);
var southWestCorner = dest(centreLAT,centreLNG,225,10);
EDIT
The above was they way to do it way back in 2011 when I wrote it. These days the google maps api has come on a loooong way. The answer by #wprater is much neater and uses some of the newer api methods.
Wouldn't it work to simply add/subtract d/2 to your x/y locations?
Given x,y as the center point:
NW = x-(d/2),y-(d/2)
SE = x+(d/2),y+(d/2)
Don't trust me on this, though - I am terrible at math :)
This assumes d as a "diameter", rather than a radius. If "d" is the radius, don't bother with the divide-by-two part.