pixymaps.js
(function(){pixymaps = {version: "0.0.1"};
var cache = {},
head = null,
tail = null,
size = 0,
maxSize = 512;
function pixymaps_cache(key, callback) {
var value = cache[key];
if (value) {
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;
}
return value.callbacks
? value.callbacks.push(callback)
: callback(value.value);
}
value = cache[key] = {
key: key,
next: head,
previous: null,
callbacks: [callback]
};
if (head) head.previous = value;
else tail = value;
head = value;
size++;
flush();
pixymaps_queue(key, function(image) {
var callbacks = value.callbacks;
delete value.callbacks;
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();
var dz = viewZoom - (viewZoom = zoom(viewZoom)),
kz = Math.pow(2, -dz);
var c0 = view.coordinate([0, 0]),
c1 = view.coordinate([viewSize[0], 0]),
c2 = view.coordinate(viewSize),
c3 = view.coordinate([0, viewSize[1]]);
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;
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];
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]); }
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;
tiles.forEach(function(tile) {
var key = url(tile);
return key == null ? done() : pixymaps_cache(key, function(image) {
context.drawImage(image, dx * (tile[0] - x0), dy * (tile[1] - y0));
done();
});
function done() {
if (!--remaining && callback) {
callback();
}
}
});
return image;
};
return image;
};
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]
};
}
function scanSpans(e0, e1, load) {
var y0 = Math.floor(e1.y0),
y1 = Math.ceil(e1.y1);
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;
}
var m0 = e0.dx / e0.dy,
m1 = e1.dx / e1.dy,
d0 = e0.dx > 0,
d1 = e1.dx < 0;
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);
}
}
}
function scanTriangle(a, b, c, load) {
var ab = edge(a, b),
bc = edge(b, c),
ca = edge(c, a);
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; }
if (ab.dy) scanSpans(ca, ab, load);
if (bc.dy) scanSpans(ca, bc, load);
}
var hosts = {},
hostRe = /^(?:([^:\/?\#]+):)?(?:\/\/([^\/?\#]*))?([^?\#]*)(?:\?([^\#]*))?(?:\#(.*))?/,
maxActive = 4,
maxAttempts = 4;
function pixymaps_queue(uri, callback) {
var hostname = (hostRe.lastIndex = 0, hostRe).exec(uri)[2] || "";
var host = hosts[hostname] || (hosts[hostname] = {
active: 0,
queued: []
});
load.attempt = 0;
host.queued.push(load);
process(host);
function load() {
var image = new Image();
image.onload = end;
image.onerror = error;
image.src = uri;
}
function end() {
host.active--;
callback(this);
process(host);
}
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";
function format(c) {
var x = c[0], y = c[1], z = c[2], max = 1 << z;
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,
angleSin = 0,
angleCosi = 1,
angleSini = 0;
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]
];
};
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);
if (arguments.length < 3) coordinate = view.coordinate(point);
var point2 = zoomBy(x).point(coordinate);
return view.panBy([point[0] - point2[0], point[1] - point2[1]]);
};
view.rotateBy = function(x) {
return view.angle(angle + x);
};
return view;
};
})()