Skip to content

Instantly share code, notes, and snippets.

@miccolis
Last active August 29, 2015 14:01
Show Gist options
  • Save miccolis/81a48232fb63e29b15f8 to your computer and use it in GitHub Desktop.
Save miccolis/81a48232fb63e29b15f8 to your computer and use it in GitHub Desktop.
CSS Transform Tiles
<html>
<head>
<style>
div#main {
overflow: hidden;
position: relative;
height: 100%;
width: 100%;
}
div.tile {
position: absolute;
width: 256px;
height: 256px;
background: #aac;
text-align: center;
line-height: 256px;
font-size: 22px;
font-family: sans-serif;
}
</style>
</head>
<body>
<div id='main'>
</div>
<script src="http://d3js.org/d3.v3.min.js" charset="utf-8"></script>
<script src="./sphericalmercator.js" charset="utf-8"></script>
<script>
(function() {
var convert = new SphericalMercator();
function currentGrid(translate, dimension, z) {
var bbox = [
-translate[0], // left
-translate[1] + dimension[1], // bottom
-translate[0] + dimension[0], // right
-translate[1] // top
];
var bounds = {
minX: Math.floor(bbox[0] / 256),
minY: Math.floor(bbox[3] / 256),
maxX: Math.floor((bbox[2] - 1) / 256),
maxY: Math.floor((bbox[1] - 1) / 256)
};
bounds.minY = bounds.minY > 0 ? bounds.minY : 0;
var tiles = [];
for (var x = bounds.minX; x <= bounds.maxX; x++) {
for (var y = bounds.minY; y <= bounds.maxY; y++) {
tiles.push({x: x, y: y, z: z});
}
}
return {z: z, tiles: tiles};
}
function bindData(translate, dimension , z) {
var selection = d3.select('#main').selectAll('div.level')
var existing = selection.data();
var current = currentGrid(translate, dimension, z);
var data;
if (existing.length) {
data = existing;
var idx = data.reduce(function(m, v, i) {
return m || (z == v.z ? i : false)
}, false);
if (!idx === false) {
data.push(current);
} else {
// Updating an existing layer, determine which tiles are already
// in place.
var add = [];
current.tiles.forEach(function(v) {
var present = data[idx].tiles.reduce(function(m, i) {
return m || (v.z == i.z && v.x == i.x && v.y == i.y)
}, false);
if (!present) add.push(v);
});
Array.prototype.push.apply(data[idx].tiles, add);
}
} else {
data = [current];
}
var level = selection.data(data);
level.enter().append('div')
.attr('class', 'level')
.attr('style', function(d) {
return '-webkit-transform: matrix(1, 0, 0, 1, '+translate[0]+','+translate[1]+');'
});
level.exit().remove();
var tiles = level.selectAll('div.tile')
.data(function(d, i) { return d.tiles; });
tiles.enter().append('div')
.attr('class', 'tile')
.text(function(d) {
var x = Math.abs(d.x % Math.pow(2, d.z));
return d.z +'/'+ x +'/'+ d.y;
})
.attr('style', function(d) {
var style = '';
var tx = 256 * d.x;
var ty = 256 * d.y;
style += '-webkit-transform: matrix(1, 0, 0, 1, '+tx+', '+ty+');'
var url = 'http://a.tiles.mapbox.com/v3/miccolis.i7fd3jil/{z}/{x}/{y}.png'
url = url.replace(/\{(z|x|y)\}/g, function(m, v) { return d[v]; });
style += 'background: url('+url+') top left no-repeat;'
return style;
});
tiles.exit().remove();
return level;
}
function ready() {
var center = [3,0,0]; // z, x, y
var hash = window.location.hash.match(/#([\d]+),([-.\d]+),([-.\d]+)/);
if (hash) center = hash.slice(1,4);
var rect = document.querySelector('#main').getBoundingClientRect();
var d = [rect.width, rect.height];
var z = center[0];
var c = convert.px([center[1], center[2]], center[0]);
var t = [
-(c[0] - d[0]/2), // left
-(c[1] - d[1]/2), // top
];
function zoomed() {
var t = d3.event.translate;
var s = d3.event.scale;
d3.select(this).attr('style', function(d) {
return '-webkit-transform: matrix('+s+', 0, 0, '+s+', '+t[0]+', '+t[1]+');'
});
bindData(t, d, z);
}
var zoom = d3.behavior.zoom()
.translate(t)
.size(d)
.on('zoom', zoomed);
var level = bindData(t, d, z);
level.call(zoom);
}
document.addEventListener('DOMContentLoaded', ready);
})();
</script>
</body>
</html>
var SphericalMercator = (function(){
// Closures including constants and other precalculated values.
var cache = {},
EPSLN = 1.0e-10,
D2R = Math.PI / 180,
R2D = 180 / Math.PI,
// 900913 properties.
A = 6378137,
MAXEXTENT = 20037508.34;
// SphericalMercator constructor: precaches calculations
// for fast tile lookups.
function SphericalMercator(options) {
options = options || {};
this.size = options.size || 256;
if (!cache[this.size]) {
var size = this.size;
var c = cache[this.size] = {};
c.Bc = [];
c.Cc = [];
c.zc = [];
c.Ac = [];
for (var d = 0; d < 30; d++) {
c.Bc.push(size / 360);
c.Cc.push(size / (2 * Math.PI));
c.zc.push(size / 2);
c.Ac.push(size);
size *= 2;
}
}
this.Bc = cache[this.size].Bc;
this.Cc = cache[this.size].Cc;
this.zc = cache[this.size].zc;
this.Ac = cache[this.size].Ac;
};
// Convert lon lat to screen pixel value
//
// - `ll` {Array} `[lon, lat]` array of geographic coordinates.
// - `zoom` {Number} zoom level.
SphericalMercator.prototype.px = function(ll, zoom) {
var d = this.zc[zoom];
var f = Math.min(Math.max(Math.sin(D2R * ll[1]), -0.9999), 0.9999);
var x = Math.round(d + ll[0] * this.Bc[zoom]);
var y = Math.round(d + 0.5 * Math.log((1 + f) / (1 - f)) * (-this.Cc[zoom]));
(x > this.Ac[zoom]) && (x = this.Ac[zoom]);
(y > this.Ac[zoom]) && (y = this.Ac[zoom]);
//(x < 0) && (x = 0);
//(y < 0) && (y = 0);
return [x, y];
};
// Convert screen pixel value to lon lat
//
// - `px` {Array} `[x, y]` array of geographic coordinates.
// - `zoom` {Number} zoom level.
SphericalMercator.prototype.ll = function(px, zoom) {
var g = (px[1] - this.zc[zoom]) / (-this.Cc[zoom]);
var lon = (px[0] - this.zc[zoom]) / this.Bc[zoom];
var lat = R2D * (2 * Math.atan(Math.exp(g)) - 0.5 * Math.PI);
return [lon, lat];
};
// Convert tile xyz value to bbox of the form `[w, s, e, n]`
//
// - `x` {Number} x (longitude) number.
// - `y` {Number} y (latitude) number.
// - `zoom` {Number} zoom.
// - `tms_style` {Boolean} whether to compute using tms-style.
// - `srs` {String} projection for resulting bbox (WGS84|900913).
// - `return` {Array} bbox array of values in form `[w, s, e, n]`.
SphericalMercator.prototype.bbox = function(x, y, zoom, tms_style, srs) {
// Convert xyz into bbox with srs WGS84
if (tms_style) {
y = (Math.pow(2, zoom) - 1) - y;
}
// Use +y to make sure it's a number to avoid inadvertent concatenation.
var ll = [x * this.size, (+y + 1) * this.size]; // lower left
// Use +x to make sure it's a number to avoid inadvertent concatenation.
var ur = [(+x + 1) * this.size, y * this.size]; // upper right
var bbox = this.ll(ll, zoom).concat(this.ll(ur, zoom));
// If web mercator requested reproject to 900913.
if (srs === '900913') {
return this.convert(bbox, '900913');
} else {
return bbox;
}
};
// Convert bbox to xyx bounds
//
// - `bbox` {Number} bbox in the form `[w, s, e, n]`.
// - `zoom` {Number} zoom.
// - `tms_style` {Boolean} whether to compute using tms-style.
// - `srs` {String} projection of input bbox (WGS84|900913).
// - `@return` {Object} XYZ bounds containing minX, maxX, minY, maxY properties.
SphericalMercator.prototype.xyz = function(bbox, zoom, tms_style, srs) {
// If web mercator provided reproject to WGS84.
if (srs === '900913') {
bbox = this.convert(bbox, 'WGS84');
}
var ll = [bbox[0], bbox[1]]; // lower left
var ur = [bbox[2], bbox[3]]; // upper right
var px_ll = this.px(ll, zoom);
var px_ur = this.px(ur, zoom);
// Y = 0 for XYZ is the top hence minY uses px_ur[1].
var bounds = {
minX: Math.floor(px_ll[0] / this.size),
minY: Math.floor(px_ur[1] / this.size),
maxX: Math.floor((px_ur[0] - 1) / this.size),
maxY: Math.floor((px_ll[1] - 1) / this.size)
};
if (tms_style) {
var tms = {
minY: (Math.pow(2, zoom) - 1) - bounds.maxY,
maxY: (Math.pow(2, zoom) - 1) - bounds.minY
};
bounds.minY = tms.minY;
bounds.maxY = tms.maxY;
}
return bounds;
};
// Convert projection of given bbox.
//
// - `bbox` {Number} bbox in the form `[w, s, e, n]`.
// - `to` {String} projection of output bbox (WGS84|900913). Input bbox
// assumed to be the "other" projection.
// - `@return` {Object} bbox with reprojected coordinates.
SphericalMercator.prototype.convert = function(bbox, to) {
if (to === '900913') {
return this.forward(bbox.slice(0, 2)).concat(this.forward(bbox.slice(2,4)));
} else {
return this.inverse(bbox.slice(0, 2)).concat(this.inverse(bbox.slice(2,4)));
}
};
// Convert lon/lat values to 900913 x/y.
SphericalMercator.prototype.forward = function(ll) {
var xy = [
A * ll[0] * D2R,
A * Math.log(Math.tan((Math.PI*0.25) + (0.5 * ll[1] * D2R)))
];
// if xy value is beyond maxextent (e.g. poles), return maxextent.
(xy[0] > MAXEXTENT) && (xy[0] = MAXEXTENT);
(xy[0] < -MAXEXTENT) && (xy[0] = -MAXEXTENT);
(xy[1] > MAXEXTENT) && (xy[1] = MAXEXTENT);
(xy[1] < -MAXEXTENT) && (xy[1] = -MAXEXTENT);
return xy;
};
// Convert 900913 x/y values to lon/lat.
SphericalMercator.prototype.inverse = function(xy) {
return [
(xy[0] * R2D / A),
((Math.PI*0.5) - 2.0 * Math.atan(Math.exp(-xy[1] / A))) * R2D
];
};
return SphericalMercator;
})();
if (typeof module !== 'undefined' && typeof exports !== 'undefined') {
module.exports = exports = SphericalMercator;
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment