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.
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; | |
| }; |