block by mbostock 1177827

Pixymaps (Dragging)

Full Screen

index.html

<!DOCTYPE html>
<script src="//d3js.org/d3.v3.min.js"></script>
<script src="pixymaps.js"></script>
<style>

body {
  font: 10px sans-serif;
}

#container {
  width: 960px;
  height: 500px;
  overflow: hidden;
}

</style>
<div id="container">
  <canvas id="map"></canvas>
</div>
<script>

var canvas = d3.select("#map").call(drag),
    context = canvas.node().getContext("2d");

var w = 960,
    h = 500,
    lon = -122.41948,
    lat = 37.76487;

var project = d3.geo.mercator()
    .scale(1 / (2 * Math.PI))
    .translate([.5, .5]);

var view = pixymaps.view()
    .size([w, h])
    .center(project([lon, lat]))
    .zoom(12);

var image = pixymaps.image()
    .view(view)
    .url(pixymaps.url("//{S}tile.cloudmade.com"
    + "/1a1b06b230af4efdbb989ea99e9841af" // //cloudmade.com/register
    + "/999/256/{Z}/{X}/{Y}.png")
    .hosts(["a.", "b.", "c.", ""]))
    .render(canvas.node());

function drag(selection) {
  var p0;

  selection
      .on("mousedown", mousedown);

  d3.select(window)
      .on("mousemove", mousemove)
      .on("mouseup", mouseup);

  function mousedown() {
    p0 = [d3.event.pageX, d3.event.pageY];
    d3.event.preventDefault();
  }

  function mousemove() {
    if (p0) {
      var p1 = [d3.event.pageX, d3.event.pageY];
      view.panBy([p1[0] - p0[0], p1[1] - p0[1]]);
      image.render(canvas.node());
      p0 = p1;
      d3.event.preventDefault();
    }
  }

  function mouseup() {
    if (p0) {
      p0 = null;
      d3.event.preventDefault();
    }
  }
}

</script>
<div id="copy">
  &copy; 2011
  <a href="//www.cloudmade.com/">CloudMade</a>,
  <a href="//www.openstreetmap.org/">OpenStreetMap</a> contributors,
  <a href="//creativecommons.org/licenses/by-sa/2.0/">CCBYSA</a>.
</div>

pixymaps.js

(function(){pixymaps = {version: "0.0.1"}; // semver
var cache = {},
    head = null,
    tail = null,
    size = 0,
    maxSize = 512;

function pixymaps_cache(key, callback) {
  var value = cache[key];

  // If this value is in the cacheā€¦
  if (value) {

    // Move it to the front of the least-recently used list.
    if (value.previous) {
      value.previous.next = value.next;
      if (value.next) value.next.previous = value.previous;
      else tail = value.previous;
      value.previous = null;
      value.next = head;
      head.previous = value;
      head = value;
    }

    // If the value is loaded, callback.
    // Otherwise, add the callback to the list.
    return value.callbacks
        ? value.callbacks.push(callback)
        : callback(value.value);
  }

  // Otherwise, add the value to the cache.
  value = cache[key] = {
    key: key,
    next: head,
    previous: null,
    callbacks: [callback]
  };

  // Add the value to the front of the least-recently used list.
  if (head) head.previous = value;
  else tail = value;
  head = value;
  size++;

  // Flush any extra values.
  flush();

  // Load the requested resource!
  pixymaps_queue(key, function(image) {
    var callbacks = value.callbacks;
    delete value.callbacks; // must be deleted before callback!
    value.value = image;
    callbacks.forEach(function(callback) { callback(image); });
  });
};

function flush() {
  for (var value = tail; size > maxSize && value; value = value.previous) {
    size--;
    delete cache[value.key];
    if (value.next) value.next.previous = value.previous;
    else if (tail = value.previous) tail.next = null;
    if (value.previous) value.previous.next = value.next;
    else if (head = value.next) head.previous = null;
  }
}
pixymaps.image = function() {
  var image = {},
      view,
      url,
      zoom = Math.round;

  image.view = function(x) {
    if (!arguments.length) return view;
    view = x;
    return image;
  };

  image.url = function(x) {
    if (!arguments.length) return url;
    url = typeof x === "string" && /{.}/.test(x) ? _url(x) : x;
    return image;
  };

  image.zoom = function(x) {
    if (!arguments.length) return zoom;
    zoom = typeof x === "function" ? x : function() { return x; };
    return image;
  };

  image.render = function(canvas, callback) {
    var context = canvas.getContext("2d"),
        viewSize = view.size(),
        viewAngle = view.angle(),
        viewCenter = view.center(),
        viewZoom = viewCenter[2],
        coordinateSize = view.coordinateSize();

    // compute the zoom offset and scale
    var dz = viewZoom - (viewZoom = zoom(viewZoom)),
        kz = Math.pow(2, -dz);

    // compute the coordinates of the four corners
    var c0 = view.coordinate([0, 0]),
        c1 = view.coordinate([viewSize[0], 0]),
        c2 = view.coordinate(viewSize),
        c3 = view.coordinate([0, viewSize[1]]);

    // apply the zoom offset to our coordinates
    c0[0] *= kz; c1[0] *= kz; c2[0] *= kz; c3[0] *= kz;
    c0[1] *= kz; c1[1] *= kz; c2[1] *= kz; c3[1] *= kz;
    c0[2] =      c1[2] =      c2[2] =      c3[2] -= dz;

    // compute the bounding box
    var x0 = Math.floor(Math.min(c0[0], c1[0], c2[0], c3[0])),
        x1 = Math.ceil(Math.max(c0[0], c1[0], c2[0], c3[0])),
        y0 = Math.floor(Math.min(c0[1], c1[1], c2[1], c3[1])),
        y1 = Math.ceil(Math.max(c0[1], c1[1], c2[1], c3[1])),
        dx = coordinateSize[0],
        dy = coordinateSize[1];

    // compute the set of visible tiles using scan conversion
    var tiles = [], z = c0[2], remaining = 0;
    scanTriangle(c0, c1, c2, push);
    scanTriangle(c2, c3, c0, push);
    function push(x, y) { remaining = tiles.push([x, y, z]); }

    // set the canvas size and transform
    var tx = viewSize[0] / 2 + dx * (x0 - viewCenter[0] * kz) | 0,
        ty = viewSize[1] / 2 + dy * (y0 - viewCenter[1] * kz) | 0;
    canvas.style.webkitTransform = "matrix3d(1,0,0,0,0,1,0,0,0,0,1,0," + tx + "," + ty + ",0,1)";
    canvas.width = (x1 - x0) * dx;
    canvas.height = (y1 - y0) * dy;

    // load each tile (hopefully from the cache) and draw it to the canvas
    tiles.forEach(function(tile) {
      var key = url(tile);

      // If there's something to show for this tile, show it.
      return key == null ? done() : pixymaps_cache(key, function(image) {
        context.drawImage(image, dx * (tile[0] - x0), dy * (tile[1] - y0));
        done();
      });

      // if that was the last tile, callback!
      function done() {
        if (!--remaining && callback) {
          callback();
        }
      }
    });

    return image;
  };

  return image;
};

// scan-line conversion
function edge(a, b) {
  if (a[1] > b[1]) { var t = a; a = b; b = t; }
  return {
    x0: a[0],
    y0: a[1],
    x1: b[0],
    y1: b[1],
    dx: b[0] - a[0],
    dy: b[1] - a[1]
  };
}

// scan-line conversion
function scanSpans(e0, e1, load) {
  var y0 = Math.floor(e1.y0),
      y1 = Math.ceil(e1.y1);

  // sort edges by x-coordinate
  if ((e0.x0 == e1.x0 && e0.y0 == e1.y0)
      ? (e0.x0 + e1.dy / e0.dy * e0.dx < e1.x1)
      : (e0.x1 - e1.dy / e0.dy * e0.dx < e1.x0)) {
    var t = e0; e0 = e1; e1 = t;
  }

  // scan lines!
  var m0 = e0.dx / e0.dy,
      m1 = e1.dx / e1.dy,
      d0 = e0.dx > 0, // use y + 1 to compute x0
      d1 = e1.dx < 0; // use y + 1 to compute x1
  for (var y = y0; y < y1; y++) {
    var x0 = Math.ceil(m0 * Math.max(0, Math.min(e0.dy, y + d0 - e0.y0)) + e0.x0),
        x1 = Math.floor(m1 * Math.max(0, Math.min(e1.dy, y + d1 - e1.y0)) + e1.x0);
    for (var x = x1; x < x0; x++) {
      load(x, y);
    }
  }
}

// scan-line conversion
function scanTriangle(a, b, c, load) {
  var ab = edge(a, b),
      bc = edge(b, c),
      ca = edge(c, a);

  // sort edges by y-length
  if (ab.dy > bc.dy) { var t = ab; ab = bc; bc = t; }
  if (ab.dy > ca.dy) { var t = ab; ab = ca; ca = t; }
  if (bc.dy > ca.dy) { var t = bc; bc = ca; ca = t; }

  // scan span! scan span!
  if (ab.dy) scanSpans(ca, ab, load);
  if (bc.dy) scanSpans(ca, bc, load);
}
var hosts = {},
    hostRe = /^(?:([^:\/?\#]+):)?(?:\/\/([^\/?\#]*))?([^?\#]*)(?:\?([^\#]*))?(?:\#(.*))?/,
    maxActive = 4, // per host
    maxAttempts = 4; // per uri

function pixymaps_queue(uri, callback) {
  var hostname = (hostRe.lastIndex = 0, hostRe).exec(uri)[2] || "";

  // Retrieve the host-specific queue.
  var host = hosts[hostname] || (hosts[hostname] = {
    active: 0,
    queued: []
  });

  // Process the host's queue, perhaps immediately starting our request.
  load.attempt = 0;
  host.queued.push(load);
  process(host);

  // Issue the HTTP request.
  function load() {
    var image = new Image();
    image.onload = end;
    image.onerror = error;
    image.src = uri;
  }

  // Handle the HTTP response.
  // Hooray, callback our available data!
  function end() {
    host.active--;
    callback(this);
    process(host);
  }

  // Boo, an error occurred. We should retry, maybe.
  function error(error) {
    host.active--;
    if (++load.attempt < maxAttempts) {
      host.queued.push(load);
    } else {
      callback(null);
    }
    process(host);
  }
};

function process(host) {
  if (host.active >= maxActive || !host.queued.length) return;
  host.active++;
  host.queued.pop()();
}
pixymaps.url = function(template) {
  var hosts = [],
      repeat = "repeat-x"; // repeat, repeat-y, no-repeat

  function format(c) {
    var x = c[0], y = c[1], z = c[2], max = 1 << z;

    // Repeat-x and repeat-y.
    if (/^repeat(-x)?$/.test(repeat) && (x = x % max) < 0) x += max;
    if (/^repeat(-y)?$/.test(repeat) && (y = y % max) < 0) y += max;
    if (z < 0 || x < 0 || x >= max || y < 0 || y >= max) return null;

    return template.replace(/{(.)}/g, function(s, v) {
      switch (v) {
        case "X": return x;
        case "Y": return y;
        case "Z": return z;
        case "S": return hosts[Math.abs(x + y + z) % hosts.length];
      }
      return v;
    });
  }

  format.template = function(x) {
    if (!arguments.length) return template;
    template = x;
    return format;
  };

  format.hosts = function(x) {
    if (!arguments.length) return hosts;
    hosts = x;
    return format;
  };

  format.repeat = function(x) {
    if (!arguments.length) return repeat;
    repeat = x;
    return format;
  };

  return format;
};
pixymaps.view = function() {
  var view = {},
      size = [0, 0],
      coordinateSize = [256, 256],
      center = [.5, .5, 0],
      angle = 0,
      angleCos = 1, // Math.cos(angle)
      angleSin = 0, // Math.sin(angle)
      angleCosi = 1, // Math.cos(-angle)
      angleSini = 0; // Math.sin(-angle)

  view.point = function(coordinate) {
    var kc = Math.pow(2, center[2] - (coordinate.length < 3 ? 0 : coordinate[2])),
        dx = (coordinate[0] * kc - center[0]) * coordinateSize[0],
        dy = (coordinate[1] * kc - center[1]) * coordinateSize[1];
    return [
      size[0] / 2 + angleCos * dx - angleSin * dy,
      size[1] / 2 + angleSin * dx + angleCos * dy
    ];
  };

  view.coordinate = function(point) {
    var dx = (point[0] - size[0] / 2);
        dy = (point[1] - size[1] / 2);
    return [
      center[0] + (angleCosi * dx - angleSini * dy) / coordinateSize[0],
      center[1] + (angleSini * dx + angleCosi * dy) / coordinateSize[1],
      center[2]
    ];
  };

  // The number of points in a coordinate at zoom level 0.
  view.coordinateSize = function(x) {
    if (!arguments.length) return coordinateSize;
    coordinateSize = x;
    return view;
  };

  view.size = function(x) {
    if (!arguments.length) return size;
    size = x;
    return view;
  };

  view.center = function(x) {
    if (!arguments.length) return center;
    center = x;
    if (center.length < 3) center[2] = 0;
    return view;
  };

  view.zoom = function(x) {
    if (!arguments.length) return center[2];
    return zoomBy(x - center[2]);
  };

  view.angle = function(x) {
    if (!arguments.length) return angle;
    angle = x;
    angleCos = Math.cos(angle);
    angleSin = Math.sin(angle);
    angleCosi = Math.cos(-angle);
    angleSini = Math.sin(-angle);
    return view;
  };

  view.panBy = function(x) {
    return view.center([
      center[0] - (angleSini * x[1] + angleCosi * x[0]) / coordinateSize[0],
      center[1] - (angleCosi * x[1] - angleSini * x[0]) / coordinateSize[1],
      center[2]
    ]);
  };

  function zoomBy(x) {
    var k = Math.pow(2, x);
    return view.center([
      center[0] * k,
      center[1] * k,
      center[2] + x
    ]);
  }

  view.zoomBy = function(x, point, coordinate) {
    if (arguments.length < 2) return zoomBy(x);

    // compute the coordinate of the center point
    if (arguments.length < 3) coordinate = view.coordinate(point);

    // compute the new point of the coordinate
    var point2 = zoomBy(x).point(coordinate);

    // pan so that the point and coordinate match after zoom
    return view.panBy([point[0] - point2[0], point[1] - point2[1]]);
  };

  view.rotateBy = function(x) {
    return view.angle(angle + x);
  };

  return view;
};
})()