Skip to content

Instantly share code, notes, and snippets.

@emeeks
Last active August 30, 2015 21:38
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 1 You must be signed in to fork a gist
  • Save emeeks/f3105fda25ff785dc5ed to your computer and use it in GitHub Desktop.
Save emeeks/f3105fda25ff785dc5ed to your computer and use it in GitHub Desktop.
Loading Tile Layers in d3.carto.map

Loading multiple tile layers with d3.carto.map.

Each layer is represented in the layer selector and can be hidden or displayed by clicking its name or the checkbox.

// Copyright 2014, Jason Davies, http://www.jasondavies.com/
(function() {
d3.geo.raster = function(projection) {
var path = d3.geo.path().projection(projection),
url = null,
scaleExtent = [0, Infinity],
subdomains = ["a", "b", "c", "d"];
var reprojectDispatch = d3.dispatch('reprojectcomplete');
var imgCanvas = document.createElement("canvas"),
imgContext = imgCanvas.getContext("2d");
function redraw(layer) {
// TODO improve zoom level computation
var z = Math.max(scaleExtent[0], Math.min(scaleExtent[1], (Math.log(projection.scale()) / Math.LN2 | 0) - 6)),
pot = z + 6,
ds = projection.scale() / (1 << pot),
t = projection.translate();
layer.style(prefix + "transform", "translate(" + t.map(pixel) + ")scale(" + ds + ")");
var tile = layer.selectAll(".tile")
.data(d3.quadTiles(projection, z), key);
tile.enter().append("canvas")
.attr("class", "tile")
.each(function(d) {
var canvas = this,
image = d.image = new Image,
k = d.key;
image.crossOrigin = true;
image.onload = function() { setTimeout(function() { onload(d, canvas, pot); }, 1); };
image.src = url({x: k[0], y: k[1], z: k[2], subdomain: subdomains[(k[0] * 31 + k[1]) % subdomains.length]});
})
.transition()
.delay(500)
.each("end", function() {reprojectDispatch.reprojectcomplete()});
tile.exit().remove();
}
redraw.url = function(_) {
if (!arguments.length) return url;
url = typeof _ === "string" ? urlTemplate(_) : _;
return redraw;
};
redraw.scaleExtent = function(_) {
return arguments.length ? (scaleExtent = _, redraw) : scaleExtent;
};
redraw.subdomains = function(_) {
return arguments.length ? (subdomains = _, redraw) : subdomains;
};
d3.rebind(redraw, reprojectDispatch, "on");
return redraw;
function onload(d, canvas, pot) {
var t = projection.translate(),
s = projection.scale(),
c = projection.clipExtent(),
image = d.image,
dx = image.width,
dy = image.height,
k = d.key,
width = 1 << k[2];
projection.translate([0, 0]).scale(1 << pot).clipExtent(null);
imgCanvas.width = dx, imgCanvas.height = dy;
imgContext.drawImage(image, 0, 0, dx, dy);
var bounds = path.bounds(d),
x0 = d.x0 = bounds[0][0] | 0,
y0 = d.y0 = bounds[0][1] | 0,
x1 = bounds[1][0] + 1 | 0,
y1 = bounds[1][1] + 1 | 0;
var Lambda0 = k[0] / width * 360 - 180,
Lambda1 = (k[0] + 1) / width * 360 - 180,
Phi0 = k[1] / width * 360 - 180,
Phi1 = (k[1] + 1) / width * 360 - 180;
mPhi0 = mercatorPhi(Phi0),
mPhi1 = mercatorPhi(Phi1);
var width = canvas.width = x1 - x0,
height = canvas.height = y1 - y0,
context = canvas.getContext("2d");
if (width > 0 && height > 0) {
var sourceData = imgContext.getImageData(0, 0, dx, dy).data,
target = context.createImageData(width, height),
targetData = target.data,
interpolate = bilinear(function(x, y, offset) {
return sourceData[(y * dx + x) * 4 + offset];
});
for (var y = y0, i = -1; y < y1; ++y) {
for (var x = x0; x < x1; ++x) {
var p = projection.invert([x, y]), Lambda, Phi;
if (!p || isNaN(Lambda = p[0]) || isNaN(Phi = p[1]) || Lambda > Lambda1 || Lambda < Lambda0 || Phi > mPhi0 || Phi < mPhi1) { i += 4; continue; }
Phi = mercatorPhi.invert(Phi);
var sx = (Lambda - Lambda0) / (Lambda1 - Lambda0) * dx,
sy = (Phi - Phi0) / (Phi1 - Phi0) * dy;
if (1) {
var q = (((Lambda - Lambda0) / (Lambda1 - Lambda0) * dx | 0) + ((Phi - Phi0) / (Phi1 - Phi0) * dy | 0) * dx) * 4;
targetData[++i] = sourceData[q];
targetData[++i] = sourceData[++q];
targetData[++i] = sourceData[++q];
} else {
targetData[++i] = interpolate(sx, sy, 0);
targetData[++i] = interpolate(sx, sy, 1);
targetData[++i] = interpolate(sx, sy, 2);
}
targetData[++i] = 0xff;
}
}
context.putImageData(target, 0, 0);
}
d3.selectAll([canvas])
.style("left", x0 + "px")
.style("top", y0 + "px");
projection.translate(t).scale(s).clipExtent(c);
}
};
function key(d) { return d.key.join(", "); }
function pixel(d) { return (d | 0) + "px"; }
// Find latitude based on Mercator y-coordinate (in degrees).
function mercatorPhi(y) {
return Math.atan(Math.exp(-y * Math.PI / 180)) * 360 / Math.PI - 90;
}
mercatorPhi.invert = function(Phi) {
return -Math.log(Math.tan(Math.PI * .25 + Phi * Math.PI / 360)) * 180 / Math.PI;
};
function bilinear(f) {
return function(x, y, o) {
var x0 = Math.floor(x),
y0 = Math.floor(y),
x1 = Math.ceil(x),
y1 = Math.ceil(y);
if (x0 === x1 || y0 === y1) return f(x0, y0, o);
return (f(x0, y0, o) * (x1 - x) * (y1 - y)
+ f(x1, y0, o) * (x - x0) * (y1 - y)
+ f(x0, y1, o) * (x1 - x) * (y - y0)
+ f(x1, y1, o) * (x - x0) * (y - y0)) / ((x1 - x0) * (y1 - y0));
};
}
function urlTemplate(s) {
return function(o) {
return s.replace(/\{([^\}]+)\}/g, function(_, d) {
var v = o[d];
return v != null ? v : d === "quadkey" && quadkey(o.x, o.y, o.z);
});
};
}
function quadkey(column, row, zoom) {
var key = [];
for (var i = 1; i <= zoom; i++) {
key.push((((row >> zoom - i) & 1) << 1) | ((column >> zoom - i) & 1));
}
return key.join("");
}
})();
// Check for vendor prefixes, by Mike Bostock.
var prefix = prefixMatch(["webkit", "ms", "Moz", "O"]);
function prefixMatch(p) {
var i = -1, n = p.length, s = document.body.style;
while (++i < n) if (p[i] + "Transform" in s) return "-" + p[i].toLowerCase() + "-";
return "";
}
(function() {
d3.quadTiles = function(projection, zoom) {
var tiles = [],
width = 1 << (zoom = Math.max(0, zoom)),
step = Math.max(.2, Math.min(1, zoom * .01)),
invisible,
precision = projection.precision(),
stream = projection.precision(960).stream({
point: function() { invisible = false; },
lineStart: noop,
lineEnd: noop,
polygonStart: noop,
polygonEnd: noop
});
visit(-180, -180, 180, 180);
projection.precision(precision);
return tiles;
function visit(x1, y1, x2, y2) {
var w = x2 - x1,
m1 = mercatorφ(y1),
m2 = mercatorφ(y2),
δ = step * w;
invisible = true;
stream.polygonStart(), stream.lineStart();
for (var x = x1; x < x2 + δ / 2 && invisible; x += δ) stream.point(x, m1);
for (var y = m1; (y += δ) < m2 && invisible;) stream.point(x2, y);
for (var x = x2; x > x1 - δ / 2 && invisible; x -= δ) stream.point(x, m2);
for (var y = m2; (y -= δ) > m1 && invisible;) stream.point(x1, y);
if (invisible) stream.point(x1, m1);
stream.lineEnd(), stream.polygonEnd();
if (w <= 360 / width) {
// TODO :)
if (!invisible) tiles.push({type: "Polygon", coordinates: [
d3.range(x1, x2 + δ / 2, δ).map(function(x) { return [x, y1]; })
.concat([[x2, .5 * (y1 + y2)]])
.concat(d3.range(x2, x1 - δ / 2, -δ).map(function(x) { return [x, y2]; }))
.concat([[x1, .5 * (y1 + y2)]])
.concat([[x1, y1]]).map(function(d) { return [d[0], mercatorφ(d[1])]; })
], key: [(180 + x1) / 360 * width | 0, (180 + y1) / 360 * width | 0, zoom], centroid: [.5 * (x1 + x2), .5 * (m1 + m2)]});
} else if (!invisible) {
var x = .5 * (x1 + x2), y = .5 * (y1 + y2);
visit(x1, y1, x, y);
visit(x, y1, x2, y);
visit(x1, y, x, y2);
visit(x, y, x2, y2);
}
}
}
function noop() {}
function mercatorφ(y) {
return Math.atan(Math.exp(-y * Math.PI / 180)) * 360 / Math.PI - 90;
}
})();
path,circle,rect,polygon,ellipse,line {
vector-effect: non-scaling-stroke;
}
svg, canvas {
top: 0;
}
#d3MapZoomBox {
position: absolute;
z-index: 10;
height: 100px;
width: 25px;
top: 10px;
right: 50px;
}
#d3MapZoomBox > button {
height:25px;
width: 25px;
line-height: 25px;
}
.d3MapControlsBox > button {
font-size:22px;
font-weight:900;
border: none;
height:25px;
width:25px;
background: rgba(35,31,32,.85);
color: white;
padding: 0;
cursor: pointer;
}
.d3MapControlsBox > button:hover {
background: black;
}
#d3MapPanBox {
position: absolute;
z-index: 10;
height: 100px;
width: 25px;
top: 60px;
right: 50px;
}
#d3MapPanBox > button {
height:25px;
width: 25px;
line-height: 25px;
}
#d3MapPanBox > button#left {
position: absolute;
left: -25px;
top: 10px;
}
#d3MapPanBox > button#right {
position: absolute;
right: -25px;
top: 10px;
}
#d3MapLayerBox {
position: relative;
z-index: 10;
height: 100px;
width: 120px;
top: 10px;
left: 10px;
overflow: auto;
color: white;
background: rgba(35,31,32,.85);
}
#d3MapLayerBox > div {
margin: 5px;
border: none;
}
#d3MapLayerBox ul {
list-style: none;
padding: 0;
margin: 0;
cursor: pointer;
}
#d3MapLayerBox li {
list-style: none;
padding: 0;
}
#d3MapLayerBox li:hover {
font-weight:700;
}
#d3MapLayerBox li input {
cursor: pointer;
}
div.d3MapModal {
position: absolute;
z-index: 11;
background: rgba(35,31,32,.90);
top: 50px;
left: 50px;
color: white;
max-width: 400px;
}
div.d3MapModalContent {
width:100%;
height: 100%;
overflow: auto;
}
div.d3MapModalContent > p {
padding: 0px 20px;
margin: 5px 0;
}
div.d3MapModalContent > h1 {
padding: 0px 20px;
font-size: 20px;
}
div.d3MapModalArrow {
content: "";
width: 0;
height: 0;
border-left: 20px solid transparent;
border-right: 20px solid transparent;
border-top: 20px solid rgba(35,31,32,.90);
position: absolute;
bottom: -20px;
left: 33px;
}
#d3MapSVG {
}
rect.minimap-extent {
fill: rgba(200,255,255,0.35);
stroke: black;
stroke-width: 2px;
stroke-dasharray: 5 5;
}
circle.newpoints {
fill: black;
stroke: red;
stroke-width: 2px;
}
path.newfeatures {
fill: steelblue;
fill-opacity: .5;
stroke: pink;
stroke-width: 2px;
}
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
<title>d3.carto.map - Multiple Tile Layers</title>
<meta charset="utf-8" />
<link type="text/css" rel="stylesheet" href="d3map.css" />
<link type="text/css" rel="stylesheet" href="https://raw.githubusercontent.com/emeeks/d3-carto-map/master/examples/example.css" />
</head>
<style>
html,body {
height: 100%;
width: 100%;
margin: 0;
}
#map {
height: 100%;
width: 100%;
position: absolute;
}
.countryborders {
fill: rgba(0,0,0,0);
stroke-width: 1px;
stroke: gray;
cursor: pointer;
}
.roads {
stroke: brown;
stroke-width: 1px;
fill: none;
}
</style>
<script>
function makeSomeMaps() {
map = d3.carto.map();
d3.select("#map").call(map);
map.centerOn([-0.1275,51.507],"latlong");
tileLayer1 = d3.carto.layer.tile();
tileLayer1
.path("examples.map-zgrqqx0w")
.label("Terrain 1")
.visibility(false);
tileLayer2 = d3.carto.layer.tile();
tileLayer2
.path("elijahmeeks.map-ktkeam22")
.label("Terrain 2");
tileLayer3 = d3.carto.layer.tile();
tileLayer3
.path("examples.map-h67hf2ic")
.label("Streets")
.visibility(false);
geojsonLayer = d3.carto.layer.geojson();
geojsonLayer
.path("http://bl.ocks.org/emeeks/raw/c970c9ee3e242e90004b/world.geojson")
.label("GeoBorders")
.visibility(false)
.cssClass("countryborders")
.renderMode("canvas");
topojsonLayer = d3.carto.layer.topojson();
topojsonLayer
.path("http://bl.ocks.org/emeeks/raw/c970c9ee3e242e90004b/sample_routes.topojson")
.label("TopoRoutes")
.visibility(false)
.cssClass("roads")
.renderMode("canvas");
map.addCartoLayer(tileLayer1);
map.addCartoLayer(tileLayer2);
map.addCartoLayer(tileLayer3);
map.addCartoLayer(topojsonLayer);
map.addCartoLayer(geojsonLayer);
}
</script>
<body onload="makeSomeMaps()">
<div id="map"></div>
<footer>
<script src="http://d3js.org/d3.v3.min.js" charset="utf-8" type="text/javascript"></script>
<script src="http://d3js.org/topojson.v1.min.js" type="text/javascript">
</script>
<script src="http://d3js.org/d3.geo.projection.v0.min.js" type="text/javascript">
</script>
<script src="tile.js" type="text/javascript">
</script>
<script src="d3.quadtiles.js" type="text/javascript">
</script>
<script src="d3.geo.raster.js" type="text/javascript">
</script>
<script src="https://rawgit.com/emeeks/d3-carto-map/master/d3.carto.map.js" type="text/javascript">
</script>
</footer>
</body>
</html>
d3.geo.tile = function() {
var size = [960, 500],
scale = 256,
translate = [size[0] / 2, size[1] / 2],
zoomDelta = 0;
function tile() {
var z = Math.max(Math.log(scale) / Math.LN2 - 8, 0),
z0 = Math.round(z + zoomDelta),
k = Math.pow(2, z - z0 + 8),
origin = [(translate[0] - scale / 2) / k, (translate[1] - scale / 2) / k],
tiles = [],
cols = d3.range(Math.max(0, Math.floor(-origin[0])), Math.max(0, Math.ceil(size[0] / k - origin[0]))),
rows = d3.range(Math.max(0, Math.floor(-origin[1])), Math.max(0, Math.ceil(size[1] / k - origin[1])));
rows.forEach(function(y) {
cols.forEach(function(x) {
tiles.push([x, y, z0]);
});
});
tiles.translate = origin;
tiles.scale = k;
return tiles;
}
tile.size = function(_) {
if (!arguments.length) return size;
size = _;
return tile;
};
tile.scale = function(_) {
if (!arguments.length) return scale;
scale = _;
return tile;
};
tile.translate = function(_) {
if (!arguments.length) return translate;
translate = _;
return tile;
};
tile.zoomDelta = function(_) {
if (!arguments.length) return zoomDelta;
zoomDelta = +_;
return tile;
};
return tile;
};
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment