Implementation of Jarke J. van Wijk and Wim A.A. Nuij's smooth and efficient zooming and panning for Polymaps.
Type a place name in the search box, top right, and hit "Go!" to fly there.
Implementation of Jarke J. van Wijk and Wim A.A. Nuij's smooth and efficient zooming and panning for Polymaps.
Type a place name in the search box, top right, and hit "Go!" to fly there.
var interval; | |
function animateCenterZoom(map, l1, z1) { | |
var start = po.map.locationCoordinate(map.center()), | |
end = po.map.locationCoordinate(l1); | |
var c0 = { x: start.column, y: start.row }, | |
c1 = { x: end.column, y: end.row }; | |
// how much world can we see at zoom 0? | |
var w0 = visibleWorld(map); | |
// z1 is ds times bigger than this zoom: | |
var ds = Math.pow(2, z1 - map.zoom()); | |
// so how much world at zoom z1? | |
var w1 = w0 / ds; | |
if (interval) { | |
clearInterval(interval); | |
interval = 0; | |
} | |
// GO! | |
animateStep(map, c0, w0, c1, w1); | |
} | |
function visibleWorld(map) { | |
// how much world can we see at zoom 0? | |
var tileCenter = po.map.locationCoordinate(map.center()); | |
var topLeft = map.pointCoordinate(tileCenter, { x:0, y:0 }); | |
var bottomRight = map.pointCoordinate(tileCenter, map.size()) | |
var correction = Math.pow(2, topLeft.zoom); | |
topLeft.column /= correction; | |
bottomRight.column /= correction; | |
topLeft.row /= correction; | |
bottomRight.row /= correction; | |
topLeft.zoom = bottomRight.zoom = 0; | |
return Math.max(bottomRight.column-topLeft.column, bottomRight.row-topLeft.row); | |
} | |
/* | |
From "Smooth and efficient zooming and panning" | |
by Jarke J. van Wijk and Wim A.A. Nuij | |
You only need to understand section 3 (equations 1 through 5) | |
and then you can skip to equation 9, implemented below: | |
*/ | |
function sq(n) { return n*n; } | |
function dist(a,b) { return Math.sqrt(sq(b.x-a.x)+sq(b.y-a.y)); } | |
function lerp1(a,b,p) { return a + ((b-a) * p) } | |
function lerp2(a,b,p) { return { x: lerp1(a.x,b.x,p), y: lerp1(a.y,b.y,p) }; } | |
function cosh(x) { return (Math.exp(x) + Math.exp(-x)) / 2; } | |
function sinh(x) { return (Math.exp(x) - Math.exp(-x)) / 2; } | |
function tanh(x) { return sinh(x) / cosh(x); } | |
function animateStep(map,c0,w0,c1,w1,V,rho) { | |
// see section 6 for user testing to derive these values (they can be tuned) | |
if (V === undefined) V = 2.0; // section 6 suggests 0.9 | |
if (rho === undefined) rho = 1.42; // section 6 suggests 1.42 | |
// simple interpolation of positions will be fine: | |
var u0 = 0, | |
u1 = dist(c0,c1); | |
// i = 0 or 1 | |
function b(i) { | |
var n = sq(w1) - sq(w0) + ((i ? -1 : 1) * Math.pow(rho,4) * sq(u1-u0)); | |
var d = 2 * (i ? w1 : w0) * sq(rho) * (u1-u0); | |
return n / d; | |
} | |
// give this a b(0) or b(1) | |
function r(b) { | |
return Math.log(-b + Math.sqrt(sq(b)+1)); | |
} | |
var r0 = r(b(0)), | |
r1 = r(b(1)), | |
S = (r1-r0) / rho; // "distance" | |
function u(s) { | |
var a = w0/sq(rho), | |
b = a * cosh(r0) * tanh(rho*s + r0), | |
c = a * sinh(r0); | |
return b - c + u0; | |
} | |
function w(s) { | |
return w0 * cosh(r0) / cosh(rho*s + r0); | |
} | |
// special case | |
if (Math.abs(u0-u1) < 0.000001) { | |
if (Math.abs(w0-w1) < 0.000001) return; | |
var k = w1 < w0 ? -1 : 1; | |
S = Math.abs(Math.log(w1/w0)) / rho; | |
u = function(s) { | |
return u0; | |
} | |
w = function(s) { | |
return w0 * Math.exp(k * rho * s); | |
} | |
} | |
var t0 = Date.now(); | |
interval = setInterval(function() { | |
var t1 = Date.now(); | |
var t = (t1 - t0) / 1000.0; | |
var s = V * t; | |
if (s > S) { | |
s = S; | |
clearInterval(interval); | |
interval = 0; | |
} | |
var us = u(s); | |
var pos = lerp2(c0,c1,(us-u0)/(u1-u0)); | |
applyPos(map, pos, w(s)); | |
}, 40); | |
} | |
function applyPos(map,pos,w) { | |
var w0 = visibleWorld(map), // how much world can we see at zoom 0? | |
size = map.size(), | |
z = Math.log(w0/w) / Math.LN2, | |
p = { x: size.x / 2, y: size.y / 2 }, | |
l = po.map.coordinateLocation({ row: pos.y, column: pos.x, zoom: 0 }); | |
map.zoomBy(z, p, l); | |
} |
<!DOCTYPE html> | |
<html> | |
<head> | |
<script type="text/javascript" src="http://github.com/simplegeo/polymaps/raw/v2.2.0/polymaps.js"></script> | |
<script type="text/javascript" src="fly.js"></script> | |
<style type="text/css"> | |
html, body { | |
width: 100%; height: 100%; | |
} | |
body { | |
margin: 0; | |
background: #E5E0D9; | |
} | |
svg { | |
width: 100%; | |
height: 100%; | |
} | |
#copy { | |
position: absolute; | |
left: 0; | |
bottom: 4px; | |
padding-left: 5px; | |
font: 9px sans-serif; | |
color: #fff; | |
cursor: default; | |
} | |
#copy a { | |
color: #fff; | |
} | |
.compass .back { | |
fill: #eee; | |
fill-opacity: .8; | |
} | |
.compass .fore { | |
stroke: #999; | |
stroke-width: 1.5px; | |
} | |
.compass rect.back.fore { | |
fill: #999; | |
fill-opacity: .3; | |
stroke: #eee; | |
stroke-width: 1px; | |
shape-rendering: crispEdges; | |
} | |
.compass .direction { | |
fill: none; | |
} | |
.compass .chevron { | |
fill: none; | |
stroke: #999; | |
stroke-width: 5px; | |
} | |
.compass .zoom .chevron { | |
stroke-width: 4px; | |
} | |
.compass .active .chevron, .compass .chevron.active { | |
stroke: #fff; | |
} | |
.compass.active .active .direction { | |
fill: #999; | |
} | |
.compass .chevron, .compass .fore { | |
stroke: #666; | |
} | |
#map { | |
background: #132328; | |
} | |
#logo { | |
position: absolute; | |
right: 0; | |
bottom: 0; | |
pointer-events: none; | |
} | |
#copy { | |
width: 33%; | |
color: #ccc; | |
pointer-events: none; | |
} | |
input { | |
position: absolute; | |
right: 60px; | |
top: 10px; | |
} | |
button { | |
position: absolute; | |
right: 10px; | |
width: 45px; | |
top: 10px; | |
} | |
</style> | |
</head> | |
<body id="map"> | |
<script type="text/javascript" src="map.js"></script> | |
<div id="copy"></div> | |
<img id="logo"/> | |
<form id="search"><input type="search" size="32" name="q" placeholder="type a place name..." disabled/><button type="submit" name="submit" disabled>Go!</button></form> | |
</body> | |
</html> |
var po = org.polymaps; | |
var div = document.getElementById("map"); | |
var map = po.map() | |
.container(div.appendChild(po.svg("svg"))) | |
.add(po.interact()); | |
/* | |
* Load the "AerialWithLabels" metadata. "Aerial" and "Road" also work. For more | |
* information about the Imagery Metadata service, see | |
* http://msdn.microsoft.com/en-us/library/ff701716.aspx | |
* You should register for your own key at https://www.bingmapsportal.com/. | |
*/ | |
var script = document.createElement("script"); | |
script.setAttribute("type", "text/javascript"); | |
script.setAttribute("src", "http://dev.virtualearth.net" | |
+ "/REST/V1/Imagery/Metadata/AerialWithLabels" | |
+ "?key=AmT-ZC3HPevQq5IBJ7v8qiDUxrojNaqbW1zBsKF0oMNEs53p7Nk5RlAuAmwSG7bg" | |
+ "&jsonp=imageryCallback"); | |
document.body.appendChild(script); | |
function imageryCallback(data) { | |
/* Display each resource as an image layer. */ | |
var resourceSets = data.resourceSets; | |
for (var i = 0; i < resourceSets.length; i++) { | |
var resources = data.resourceSets[i].resources; | |
for (var j = 0; j < resources.length; j++) { | |
var resource = resources[j]; | |
map.add(po.image() | |
.url(template(resource.imageUrl, resource.imageUrlSubdomains))) | |
.tileSize({x: resource.imageWidth, y: resource.imageHeight}); | |
} | |
} | |
/* Display brand logo. */ | |
document.getElementById("logo").src = data.brandLogoUri; | |
/* Display copyright notice. */ | |
document.getElementById("copy").appendChild(document.createTextNode(data.copyright)); | |
/* Display compass control. */ | |
map.add(po.compass() | |
.pan("none")); | |
setUpSearch(); | |
} | |
/** Returns a Bing URL template given a string and a list of subdomains. */ | |
function template(url, subdomains) { | |
var n = subdomains.length, | |
salt = ~~(Math.random() * n); // per-session salt | |
/** Returns the given coordinate formatted as a 'quadkey'. */ | |
function quad(column, row, zoom) { | |
var key = ""; | |
for (var i = 1; i <= zoom; i++) { | |
key += (((row >> zoom - i) & 1) << 1) | ((column >> zoom - i) & 1); | |
} | |
return key; | |
} | |
return function(c) { | |
var quadKey = quad(c.column, c.row, c.zoom), | |
server = Math.abs(salt + c.column + c.row + c.zoom) % n; | |
return url | |
.replace("{quadkey}", quadKey) | |
.replace("{subdomain}", subdomains[server]); | |
}; | |
} | |
/////////////////////// search... | |
function setUpSearch() { | |
var search = document.getElementById('search'); | |
search.q.disabled = null; | |
search.submit.disabled = null; | |
search.onsubmit = function() { | |
if (search.q.value && search.q.value.length > 0) { | |
search.q.disabled = 'true'; | |
search.submit.disabled = 'true'; | |
doSearch(search.q.value); | |
} | |
return false; | |
} | |
} | |
function doSearch(q) { | |
var script = document.createElement("script"); | |
script.setAttribute("type", "text/javascript"); | |
script.setAttribute("src", "http://dev.virtualearth.net" | |
+ "/REST/V1/Locations" | |
+ "?key=AmT-ZC3HPevQq5IBJ7v8qiDUxrojNaqbW1zBsKF0oMNEs53p7Nk5RlAuAmwSG7bg" | |
+ "&query=" + encodeURIComponent(q) | |
+ "&jsonp=searchCallback"); | |
document.body.appendChild(script); | |
} | |
function searchCallback(rsp) { | |
try { | |
// console.log(rsp); | |
var bbox = rsp.resourceSets[0].resources[0].bbox; // [s,w,n,e] | |
// TODO: don't just use the first one, see if there's one nearby to where we're already looking | |
// compute the extent in points, scale factor, and center | |
// -- borrowed from map.extent(), thanks Mike | |
var bl = map.locationPoint({ lat: bbox[0], lon: bbox[1] }), | |
tr = map.locationPoint({ lat: bbox[2], lon: bbox[3] }), | |
sizeActual = map.size(), | |
k = Math.max((tr.x - bl.x) / sizeActual.x, (bl.y - tr.y) / sizeActual.y), | |
l = map.pointLocation({x: (bl.x + tr.x) / 2, y: (bl.y + tr.y) / 2}); | |
// update the zoom level | |
var z = map.zoom() - Math.log(k) / Math.log(2); | |
animateCenterZoom(map, l, z); | |
} | |
catch(e) { | |
console.error(e); | |
// TODO: what? reset map position/zoom, perhaps? show error? | |
} | |
var search = document.getElementById('search'); | |
search.q.disabled = null; | |
search.submit.disabled = null; | |
} |
Really cool !