Skip to content

Instantly share code, notes, and snippets.

@mbostock
Last active October 10, 2023 17:09
Show Gist options
  • Star 2 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save mbostock/1177827 to your computer and use it in GitHub Desktop.
Save mbostock/1177827 to your computer and use it in GitHub Desktop.
Pixymaps (Dragging)
license: gpl-3.0
<!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("http://{S}tile.cloudmade.com"
+ "/1a1b06b230af4efdbb989ea99e9841af" // http://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="http://www.cloudmade.com/">CloudMade</a>,
<a href="http://www.openstreetmap.org/">OpenStreetMap</a> contributors,
<a href="http://creativecommons.org/licenses/by-sa/2.0/">CCBYSA</a>.
</div>
(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;
};
})()
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment