Skip to content

Instantly share code, notes, and snippets.

@mbostock
Forked from RandomEtc/README.mkd
Last active November 17, 2020 18:25
  • Star 1 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
Star You must be signed in to star a gist
Save mbostock/600164 to your computer and use it in GitHub Desktop.
Van Wijk & Nuij Zooming
license: gpl-3.0
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);
}
<!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>
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;
}
@RandomEtc
Copy link

Super SLOW flying! You might also experiment with tuning V rather than changing the meaning of t. V is speed and it's only used to multiply t anyway. I bumped mine to twice what the paper suggests, but slower is good too!

PS I think it's Nuij not Nuil :)

@mbostock
Copy link
Author

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!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment