Skip to content

Instantly share code, notes, and snippets.

@RandomEtc
Created September 28, 2010 00:11
Show Gist options
  • Save RandomEtc/600144 to your computer and use it in GitHub Desktop.
Save RandomEtc/600144 to your computer and use it in GitHub Desktop.
smooth panning and zooming for polymaps
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;
}
@cmadsen
Copy link

cmadsen commented Jan 27, 2012

Really cool !

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