block by RandomEtc 600144

smooth panning and zooming for polymaps

Full Screen

index.html

<!DOCTYPE html>
<html>
  <head>
    <script type="text/javascript" src="//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>

README.mkd

Implementation of Jarke J. van Wijk and Wim A.A. Nuij's [smooth and efficient zooming and panning](http://www.win.tue.nl/~vanwijk/zoompan.pdf) for Polymaps.

Type a place name in the search box, top right, and hit "Go!" to fly there.

fly.js

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);
}

map.js

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;
}