Related
I have been trying to make this shape in svg. Problem is, I want to manipulate it with the blue handles. I have already made a simple arrow and am able to change its shape with quadratic bezier curves. But I am unable to figure out how to do it for this kind of shape. Is there some way to transform a line into this squiggly form?
You can use the getPointAtLength and getTotalLength APIs to "ride" along any arbitrary SVG geometry, and generate your sine wave.
Here's an example in plain TypeScript (find an interactive React CodeSandbox with a couple extra bells and whistles here).
function computeWave(
path: SVGPathElement,
freq: number,
maxAmp: number,
phase: number,
res: number
) {
// Get the points of the geometry with the given resolution
const length = path.getTotalLength();
const points = [];
if (res < 0.1) res = 0.1; // prevent infinite loop
for (let i = 0; i <= length + res; i += res) {
const { x, y } = path.getPointAtLength(i);
points.push([x, y]);
}
// For each of those points, generate a new point...
const sinePoints = [];
for (let i = 0; i < points.length - 1; i++) {
// Numerical computation of the angle between this and the next point
const [x0, y0] = points[i];
const [x1, y1] = points[i + 1];
const ang = Math.atan2(y1 - y0, x1 - x0);
// Turn that 90 degrees for the normal angle (pointing "left" as far
// as the geometry is considered):
const normalAngle = ang - Math.PI / 2;
// Compute the sine-wave phase at this point.
const pointPhase = ((i / (points.length - 1)) * freq - phase) * Math.PI * 2;
// Compute the sine-wave amplitude at this point.
const amp = Math.sin(pointPhase) * maxAmp;
// Apply that to the current point.
const x = x0 + Math.cos(normalAngle) * amp;
const y = y0 + Math.sin(normalAngle) * amp;
sinePoints.push([x, y]);
}
// Terminate the sine points where the shape ends.
sinePoints.push(points[points.length - 1]);
// Compute SVG polyline string.
return sinePoints
.map(([x, y], i) => `${i === 0 ? "M" : "L"}${x},${y}`)
.join(" ");
}
which generates the blue line following the orange one (which is described as M100,100 C150,100,150,250,200,200):
You can of course adapt this to e.g. "pinch" the wave at the ends, to avoid any abrupt ends with an arbitrary phase, etc.
There is are no such transformations in SVG. So you have to find equally spaced points on the bezier curve and offset them according to the sinusuidal equation.
This is a great video to explaining bezier curves and using a look up table to find equally spaced points on the arc: https://www.youtube.com/watch?v=aVwxzDHniEw
To understand how to offset the points, you need a bit co-ordinate geometry. I have created a Desmos graph to help you out: https://www.desmos.com/calculator/4lbhfcro8t
Notice that the sine curve in the above graph is not uniform. That is because the points used for offsetting are equally spaced 't' values. You have to use equally spaced arc lengths as demonstrated in the video.
I need to know what is the current center of the Cesium Map.
I tried to use viewer.camera.position but it always gives the same z value (x: 16921255.101297915, y: 5578093.302269477, z: 12756274) and I'm not sure about the x and y values. Are they in meters?
Thanks a lot!
EDIT: Solution
With all the help I got (thanks!) I put this together:
getPosition(){
if (viewer.scene.mode == 3) {
var windowPosition = new Cesium.Cartesian2(viewer.container.clientWidth / 2, viewer.container.clientHeight / 2);
var pickRay = viewer.scene.camera.getPickRay(windowPosition);
var pickPosition = viewer.scene.globe.pick(pickRay, viewer.scene);
var pickPositionCartographic = viewer.scene.globe.ellipsoid.cartesianToCartographic(pickPosition);
console.log(pickPositionCartographic.longitude * (180 / Math.PI));
console.log(pickPositionCartographic.latitude * (180 / Math.PI));
} else if (viewer.scene.mode == 2) {
var camPos = viewer.camera.positionCartographic;
console.log(camPos.longitude * (180 / Math.PI));
console.log(camPos.latitude * (180 / Math.PI));
}
};
This function gives longitude/latitude coordinates in degrees.
viewer.camera.position gives you the position at which camera is located in X,Y,Z coordinates in meters in relation to earth center.
Depending on which scene mode you are using approach is different:
SCENE3D:
In order to see at what is the camera looking at you need to get the intersect point of camera's pick ray and map.
function getMapCenter() {
var windowPosition = new Cesium.Cartesian2(viewer.container.clientWidth / 2, viewer.container.clientHeight / 2);
var pickRay = viewer.scene.camera.getPickRay(windowPosition);
var pickPosition = viewer.scene.globe.pick(pickRay, viewer.scene);
var pickPositionCartographic = viewer.scene.globe.ellipsoid.cartesianToCartographic(pickPosition);
console.log(pickPositionCartographic.longitude * (180/Math.PI));
console.log(pickPositionCartographic.latitude * (180/Math.PI));
}
Based on this thread.
Also try to check if camera is looking at the map, and not a the sky.
SCENE2D:
This is a simple 2D view with camera pointing directly down.
From docs:
2D mode. The map is viewed top-down with an orthographic projection
var camPos = viewer.camera.positionCartographic;
console.log(camPos.longitude * (180/Math.PI));
console.log(camPos.latitude * (180/Math.PI));
Remaining case is 2.5D or COLUMBUS_VIEW
I'm using WebGL globe from http://workshop.chromeexperiments.com/globe/. If any point of the globe is clicked, I need to get the longitude and latitude of that point. These parameters are to be passed to the Google Maps for 2D map.
How can I get the long. and lat. from the webgl globe?
Through this function I'm getting the double clicked point, and through this point I'm finding the long. and lat. But the results are not correct. It seems that the clicked point is not determined properly.
function onDoubleClick(event) {
event.preventDefault();
var vector = new THREE.Vector3(
( event.clientX / window.innerWidth ) * 2 - 1,
-( event.clientY / window.innerHeight ) * 2 + 1,
0.5
);
projector.unprojectVector(vector, camera);
var ray = new THREE.Ray(camera.position, vector.subSelf(camera.position).normalize());
var intersects = ray.intersectObject(globe3d);
if (intersects.length > 0) {
object = intersects[ 0 ];
console.log(object);
r = object.object.boundRadius;
x = object.point.x;
y = object.point.y;
z = object.point.z;
console.log(Math.sqrt(x * x + y * y + z * z));
lat = 90 - (Math.acos(y / r)) * 180 / Math.PI;
lon = ((270 + (Math.atan2(x, z)) * 180 / Math.PI) % 360) - 180;
console.log(lat);
console.log(lon);
}
}
Get the WebGL Globe here https://github.com/dataarts/webgl-globe/archive/master.zip
You can open it directly on Mozilla, if you open it in Chrome it works with earth surface image lack because of Cross-Origin Resource Sharing policy. It needs to be put in a virtual host.
Try to use the function in this way
function onDoubleClick(event) {
event.preventDefault();
var canvas = renderer.domElement;
var vector = new THREE.Vector3( ( (event.offsetX) / canvas.width ) * 2 - 1, - ( (event.offsetY) / canvas.height) * 2 + 1,
0.5 );
projector.unprojectVector( vector, camera );
var ray = new THREE.Ray(camera.position, vector.subSelf(camera.position).normalize());
var intersects = ray.intersectObject(globe3d);
if (intersects.length > 0) {
object = intersects[0];
r = object.object.boundRadius;
x = object.point.x;
y = object.point.y;
z = object.point.z;
lat = 90 - (Math.acos(y / r)) * 180 / Math.PI;
lon = ((270 + (Math.atan2(x, z)) * 180 / Math.PI) % 360) - 180;
lat = Math.round(lat * 100000) / 100000;
lon = Math.round(lon * 100000) / 100000;
window.location.href = 'gmaps?lat='+lat+'&lon='+lon;
}
}
I used the code you share with a little correction and it works great.
The way to let it work correctly is to understand exactly what you pass to the new THREE.Vector3.
This function need three parameters (x, y, z)
z in our/your case is sculpted as 0.5 and it's ok
x and y must be a number among -1 and 1, so, to obtain this values you need to catch the click coordinates on your canvas and then, with this formula, reduce them to a value in this range (-1...0...1);
var vectorX = ((p_coord_X / canvas.width ) * 2 - 1);
var vectorY = -((p_coord_Y / canvas.height ) * 2 - 1);
where p_coord_X and p_coord_Y are the coordinates of the click (referred to the left top corner of your canvas) and canvas is the canvas area where lives your webgl globe.
The problem is how to get the click X and Y coordinates in pixel, because it depends by how your canvas is placed in your HTML enviroment.
For my cases the solution over proposed where not suitable cause i returned always false results; so i build a solution to get extacly the x and y coordinates of my canvas area as i clicked on it (i had for my case too to insert a scrollY page correction).
Now imagine to devide in 4 square your canvas area, a click in the NW quadrant will return for example a -0.8, -05 x and y values, a click in SE i.e. a couple of 0.6, 0.4 values.
The ray.intersectObject() function that follows uses then our click-vector-converted data to understand if our click intersects the globe, if it matches, return correctly the coordinates lat and lon.
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'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());
}