A fork of Tom Carden’s implementation of Jarke J. van Wijk and Wim A.A. Nuij’s smooth and efficient zooming and panning for Polymaps.
-
-
Save mbostock/600164 to your computer and use it in GitHub Desktop.
Van Wijk & Nuij Zooming
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
license: gpl-3.0 |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 = 0.9; | |
if (rho === undefined) rho = 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) / 10000.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 l = po.map.coordinateLocation({ row: pos.y, column: pos.x, zoom: 0 }), | |
x = map.size(); | |
// how much world can we see at zoom 0? | |
var w0 = visibleWorld(map); | |
map.zoomBy(Math.log(w0/w) / Math.LN2, {x: x.x / 2, y: x.y / 2}, l); | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<!DOCTYPE html> | |
<html> | |
<head> | |
<script type="text/javascript" src="https://cdn.rawgit.com/simplegeo/polymaps/v2.3.0/polymaps.min.js"></script> | |
<script type="text/javascript" src="fly.js"></script> | |
<style type="text/css"> | |
html, body { | |
width: 100%; | |
height: 100%; | |
} | |
body { | |
margin: 0; | |
background: #000; | |
} | |
svg { | |
display: block; | |
overflow: hidden; | |
width: 100%; | |
height: 100%; | |
} | |
#copy { | |
display: none; | |
} | |
#logo { | |
display: 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" value="London"/><button type="submit" name="submit" disabled>Go!</button></form> | |
</body> | |
</html> |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
var po = org.polymaps; | |
var map = po.map() | |
.container(document.getElementById("map").appendChild(po.svg("svg"))); | |
/* | |
* 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}); | |
} | |
} | |
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; | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
I slowed it down so that I could take a reasonable screen recording (and speed it back up again in iMovie). Thanks for the clarification on
t
vs.V
, and the spell correction!