block by emeeks f3105fda25ff785dc5ed

Loading Tile Layers in d3.carto.map

Full Screen

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.

index.html

<html xmlns="//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("//bl.ocks.org/emeeks/raw/c970c9ee3e242e90004b/world.geojson")
    .label("GeoBorders")
    .visibility(false)
    .cssClass("countryborders")
    .renderMode("canvas");
    
    topojsonLayer = d3.carto.layer.topojson();
    topojsonLayer
    .path("//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="//d3js.org/d3.v3.min.js" charset="utf-8" type="text/javascript"></script>
<script src="//d3js.org/topojson.v1.min.js" type="text/javascript">
</script>
<script src="//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.raster.js

// 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 "";
}

d3.quadtiles.js

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

})();

d3map.css

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

tile.js

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