Skip to content

Instantly share code, notes, and snippets.

@Andrew-Reid
Last active February 2, 2019 02:12
Show Gist options
  • Save Andrew-Reid/bc9c9a6203982773346cdeff3d0d9b1d to your computer and use it in GitHub Desktop.
Save Andrew-Reid/bc9c9a6203982773346cdeff3d0d9b1d to your computer and use it in GitHub Desktop.
D3-slippy map with canvas tiles
// Andrew Reid 2018
(function (global, factory) {
typeof exports === 'object' && typeof module !== 'undefined' ? factory(exports) :
typeof define === 'function' && define.amd ? define(['exports'], factory) :
(factory((global.d3 = global.d3 || {}),global.d3));
}(this, function (exports) { 'use strict';
function geoTile() {
// Basic Constants
const tau = Math.PI * 2;
const lim = 85.05113;
// Map Properties:
var w = 960;
var h = 500;
// Projection Values:
var pk = w/tau; // projection scale k
var pc = [0,0] // projection geographic center
var pr = 0 // central longitude for rotation.
// Zoom Transform Values:
var tk = 1; // zoom transform scale k
var tx = w/2; // zoom transform translate x
var ty = h/2; // zoom transform translate y
// The Projection:
var p = d3.geoMercator()
.scale(pk)
.center(pc);
// Tile wrapping and zoom limits:
var z0 = 4;
var z1 = 13;
var extent = {left:-179.99999,top:lim,right:179.9999,bottom:-lim};
var wrap = false;
// Tile source & attribution
var source = function(d) {
return "http://" + "abc"[d.y % 3] + ".tile.openstreetmap.org/" + d.z + "/" + d.x + "/" + d.y + ".png";
}
var a = "Tiles © OpenStreetMap contributors";
function geoTile(_) {
return p(_);
}
// General Methods
geoTile.width = function(_) {
return arguments.length ? (w = _, geoTile) : w;
}
geoTile.height = function(_) {
return arguments.length ? (h = _, geoTile) : h;
}
geoTile.size = function(_) {
if(arguments.length) {
(_ instanceof d3.selection) ? (w = _.attr("width"), h = _.attr("height")) : (w = _[0], h = _[1]);
return geoTile;
}
else return [w,h]
}
geoTile.source = function(_) {
return arguments.length ? (source = _, geoTile) : source;
}
geoTile.projection = function() {
return p;
}
geoTile.attribution = function(_) {
return arguments.length ? (a = _, geoTile) : a;
}
geoTile.wrap = function(_) {
return arguments.length ? (wrap = _, geoTile) : wrap;
}
// Projection methods:
geoTile.invert = function(_) {
return p.invert(_);
}
geoTile.center = function(_) {
return arguments.length ? (pc = _, p.center(pc), geoTile): pc;
}
geoTile.scale = function(_) {
return arguments.length ? (pk = _, p.scale(pk), geoTile) : pk;
}
geoTile.rotate = function(_) {
return arguments.length ? (pr = _, p.rotate([pr,0]), geoTile) : pr;
}
geoTile.fit = function(_) {
return arguments.length ? (p.fitSize([w,h],_),tx = p.translate()[0],ty = p.translate()[1],pk = p.scale(), geoTile) : "n/a";
}
geoTile.fitExtent = function(e,f) {
return arguments.length > 1 ? (p.fitExtent(e,f),pk = p.scale(),tx = p.translate()[0],ty = p.translate()[1], geoTile) : "n/a";
}
geoTile.fitMargin = function(m,f) {
return arguments.length > 1 ? (p.fitExtent([[m,m],[w-m,h-m]],f), tx = p.translate()[0],ty = p.translate()[1],pk = p.scale(), geoTile) : "n/a";
}
// Zoom Methods:
geoTile.zoomScale = function(_) {
return arguments.length ? (tk = _, p.scale(pk*tk), geoTile) : tk;
}
geoTile.zoomTranslate = function(_) {
return arguments.length ? (tx = _[0], ty = _[1], p.translate([tx, ty]), geoTile): [tx,ty]
}
geoTile.zoomIdentity = function() {
return d3.zoomIdentity.translate(tx,ty).scale(tk).translate(0,0);
}
geoTile.zoomTransform = function(t) {
tx = t.x, ty = t.y, tk = t.k; p.translate([tx,ty]); p.scale(pk*tk); return geoTile;
}
// Convert between zoom k and tile depth.
geoTile.tileDepth = function(z) {
if(arguments.length) {
tk = Math.pow(Math.E, ((z + 8) * Math.LN2)) / pk / tau;
}
else {
var size = pk * tk * tau;
var z = Math.max(Math.log(size) / Math.LN2 - 8, 0);
return Math.round(z);
}
}
// Zoom extent methods:
geoTile.zoomScaleExtent = function(_) {
if (arguments.length) {
z0 = _[0];
z1 = _[1];
return geoTile;
}
else {
var size = pk * tk * tau;
var z = Math.max(Math.log(size) / Math.LN2 - 8, 0);
var max = Math.pow(2,z1)/Math.pow(2,z);
var min = Math.pow(2,z0)/Math.pow(2,z);
return [min,max];
}
}
geoTile.zoomTranslateExtent = function(_) {
if (arguments.length) {
extent.left = _[0][0];
extent.top = _[0][1];
extent.right = _[1][0];
extent.bottom = _[1][1];
return geoTile;
}
else {
var x0 = p([extent.left-pr,extent.top])[0] - tx;
var y0 = p([extent.left-pr,extent.top])[1] - ty;
var x1 = p([extent.right-pr,extent.bottom])[0] - tx;
var y1 = p([extent.right-pr,extent.bottom])[1] - ty;
return [[x0,y0],[x1,y1]];
}
}
geoTile.zoomTranslateConstrain = function() {
extent.left = p.invert([0,0])[0];
extent.top = p.invert([0,0])[1];
extent.right = p.invert([w,h])[0];
extent.bottom = p.invert([w,h])[1];
var x0 = p([extent.left-pr,extent.top])[0] - tx;
var y0 = p([extent.left-pr,extent.top])[1] - ty;
var x1 = p([extent.right-pr,extent.bottom])[0] - tx;
var y1 = p([extent.right-pr,extent.bottom])[1] - ty;
return [[x0,y0],[x1,y1]];
}
// Tile Methods:
// Calculate Tiles:
geoTile.tiles = function() {
var size = pk * tk * tau;
var z = Math.max(Math.log(size) / Math.LN2 - 8, 0); // z, assuming image size of 256 (2^8).
var s = Math.pow(2, z - Math.round(z) + 8);
var y0 = p([-180,lim])[1];
var x0 = p([-180,lim])[0];
var set = [];
var cStart = wrap ? Math.floor((0 - x0) / s) : Math.max(0, Math.floor((0 - x0) / s));
var cEnd = Math.max(0, Math.ceil((w - x0) / s));
var rStart = Math.max(0,Math.floor((0 - y0) / s));
var rEnd = Math.max(0, Math.ceil((h - y0) / s));
for(var i = cStart; i < cEnd; i++) {
for(var j = rStart; j < rEnd; j++) {
var x = i;
if (wrap) {
var k = Math.pow(2,Math.round(z));
x = (i+k)%k;
}
set.push({x:x,y:j,z:Math.round(z),tx:i,ty:j, id:i+"-"+j+"-"+z})
}
}
set.translate = [x0 / s, y0 / s];
set.scale = s;
return set;
}
// Assign Tiles to a Selection:
geoTile.tile = function(g) {
var set = geoTile.tiles();
var images = g.attr("transform", stringify(set.scale, set.translate))
.selectAll("image")
.data(set, function(d) { return d.id; })
images.exit().remove();
images.enter().append("image").merge(images)
.attr("xlink:href", source )
.attr("x", function(d) { return d.tx * 256; })
.attr("y", function(d) { return d.ty * 256; })
.attr("width", 256)
.attr("height", 256);
}
// Draw on a canvas:
geoTile.canvas = function(context) {
var set = geoTile.tiles();
var k = set.scale / 256, r = set.scale % 1 ? Number : Math.round;
var ox = r(set.translate[0] * set.scale);
var oy = r(set.translate[1] * set.scale);
set.forEach(function(d) {
var tile = new Image();
tile.src = source(d); // can also be a remote URL e.g. http://
tile.onload = function() {
context.drawImage(tile,d.tx*256*k+ox,d.ty*256*k+oy,256*k,256*k);
};
})
}
// Helper stringify
function stringify(scale, translate) {
var k = scale / 256, r = scale % 1 ? Number : Math.round;
return "translate(" + r(translate[0] * scale) + "," + r(translate[1] * scale) + ") scale(" + k + ")";
}
// To break out in the future, at least use switch ?:
geoTile.tileSet = function(_) {
if(typeof _ == "function") {
source = _;
}
// CartoDB:
else if(_ == "CartoDB_Positron") {
a = "© OpenStreetMap © CartoDB";
source = function(d) {
return "https://cartodb-basemaps-b.global.ssl.fastly.net/light_all/"+d.z+"/"+d.x+"/"+d.y+".png";
}
}
else if(_ == "CartoDB_PositronNoLabels") {
a = "© OpenStreetMap © CartoDB";
source = function(d) {
return "https://cartodb-basemaps-b.global.ssl.fastly.net/light_nolabels/"+d.z+"/"+d.x+"/"+d.y+".png";
}
}
else if(_ == "CartoDB_PositronOnlyLabels") {
a = "© OpenStreetMap © CartoDB";
source = function(d) {
return "https://cartodb-basemaps-b.global.ssl.fastly.net/light_only_labels/"+d.z+"/"+d.x+"/"+d.y+".png";
}
}
else if(_ == "CartoDB_DarkMatter") {
a = "© OpenStreetMap © CartoDB";
source = function(d) {
return "https://cartodb-basemaps-b.global.ssl.fastly.net/dark_all/"+d.z+"/"+d.x+"/"+d.y+".png";
}
}
else if(_ == "CartoDB_DarkMatterNoLabels") {
a = "© OpenStreetMap © CartoDB";
source = function(d) {
return "https://cartodb-basemaps-b.global.ssl.fastly.net/dark_nolabels/"+d.z+"/"+d.x+"/"+d.y+".png";
}
}
else if(_ == "CartoDB_DarkMatterOnlyLabels") {
a = "© OpenStreetMap © CartoDB";
source = function(d) {
return "https://cartodb-basemaps-b.global.ssl.fastly.net/dark_only_labels/"+d.z+"/"+d.x+"/"+d.y+".png";
}
}
else if(_ == "CartoDB_Voyager") {
a = "© OpenStreetMap © CartoDB";
source = function(d) {
return "https://cartodb-basemaps-b.global.ssl.fastly.net/rastertiles/voyager/"+d.z+"/"+d.x+"/"+d.y+".png";
}
}
// ESRI
else if(_ == "ESRI_WorldTerrain") {
a = "Tiles © Esri - Source: USGS, Esri, TANA, DeLorme, and NPS"
source = function(d) {
return "https://server.arcgisonline.com/ArcGIS/rest/services/World_Terrain_Base/MapServer/tile/"+d.z+"/"+d.y+"/"+d.x+".png";
}
}
else if (_ == "ESRI_WorldShadedRelief") {
a = "Tiles © Esri - Source: Esri";
source = function(d) {
return "https://server.arcgisonline.com/ArcGIS/rest/services/World_Shaded_Relief/MapServer/tile/"+d.z+"/"+d.y+"/"+d.x+".png";
}
}
else if (_ == "ESRI_WorldPhysical") {
a = "Tiles © Esri - Source: US National Park Service";
source = function(d) {
return "https://server.arcgisonline.com/ArcGIS/rest/services/World_Physical_Map/MapServer/tile/"+d.z+"/"+d.y+"/"+d.x+".png";
}
}
else if (_ == "ESRI_WorldStreetMap") {
a = "Tiles © Esri - Source: Esri, DeLorme, NAVTEQ, USGS, Intermap, iPC, NRCAN, Esri Japan, METI, Esri China (Hong Kong), Esri (Thailand), TomTom";
source = function(d) {
return "https://server.arcgisonline.com/ArcGIS/rest/services/World_Street_Map/MapServer/tile/"+d.z+"/"+d.y+"/"+d.x+".png";
}
}
else if (_ == "ESRI_WorldTopoMap") {
a = "Tiles © Esri - Source: Esri, DeLorme, NAVTEQ, TomTom, Intermap, iPC, USGS, FAO, NPS, NRCAN, GeoBase, Kadaster NL, Ordnance Survey, Esri Japan, METI, Esri China (Hong Kong), and the GIS User Community";
source = function(d) {
return "https://server.arcgisonline.com/ArcGIS/rest/services/World_Topo_Map/MapServer/tile/"+d.z+"/"+d.y+"/"+d.x+".png";
}
}
else if (_ == "ESRI_WorldImagery") {
a = "Tiles © Esri - Source: Esri, i-cubed, USDA, USGS, AEX, GeoEye, Getmapping, Aerogrid, IGN, IGP, UPR-EGP, and the GIS User Community";
source = function(d) {
return "https://server.arcgisonline.com/ArcGIS/rest/services/World_Imagery/MapServer/tile/"+d.z+"/"+d.y+"/"+d.x+".png";
}
}
else if (_ == "ESRI_OceanBasemap") {
a = "Tiles © Esri - Source: GEBCO, NOAA, CHS, OSU, UNH, CSUMB, National Geographic, DeLorme, NAVTEQ, and Esri";
source = function(d) {
return "https://server.arcgisonline.com/ArcGIS/rest/services/Ocean_Basemap/MapServer/tile/"+d.z+"/"+d.y+"/"+d.x+".png";
}
}
else if (_ == "ESRI_NGWorld") {
a = "Tiles © Esri - Source: National Geographic, Esri, DeLorme, NAVTEQ, UNEP-WCMC, USGS, NASA, ESA, METI, NRCAN, GEBCO, NOAA, iPC";
source = function(d) {
return "https://server.arcgisonline.com/ArcGIS/rest/services/NatGeo_World_Map/MapServer/tile/"+d.z+"/"+d.y+"/"+d.x+".png";
}
}
else if (_ == "ESRI_Gray") {
a = "Tiles © Esri - Source: Esri, DeLorme, NAVTEQ";
source = function(d) {
return "https://server.arcgisonline.com/ArcGIS/rest/services/Canvas/World_Light_Gray_Base/MapServer/tile/"+d.z+"/"+d.y+"/"+d.x+".png";
}
}
// OSM
else if (_ == "OSM_Topo") {
a = "Tiles © OpenStreetMap contributors";
source = function(d) {
return "https://tile.opentopomap.org/"+d.z+"/"+d.x+"/"+d.y+".png";
}
}
else if (_ == "OSM") {
a = "Tiles © OpenStreetMap contributors";
source = function(d) {
return "https://" + "abc"[d.y % 3] + ".tile.openstreetmap.org/" + d.z + "/" + d.x + "/" + d.y + ".png";
}
}
// STAMEN
else if (_ == "Stamen_Toner") {
a = "Map tiles by Stamen Design, under CC BY 3.0. Data by OpenStreetMap, under ODbL."
source = function(d) {
return "https://stamen-tiles.a.ssl.fastly.net/toner/" + d.z + "/" + d.x + "/" + d.y + ".png";
}
}
else if (_ == "Stamen_TonerBackground") {
a = "Map tiles by Stamen Design, under CC BY 3.0. Data by OpenStreetMap, under ODbL."
source = function(d) {
return "https://stamen-tiles.a.ssl.fastly.net/toner-background/" + d.z + "/" + d.x + "/" + d.y + ".png";
}
}
else if (_ == "Stamen_TonerLines") {
a = "Map tiles by Stamen Design, under CC BY 3.0. Data by OpenStreetMap, under ODbL."
source = function(d) {
return "https://stamen-tiles.a.ssl.fastly.net/toner-lines/" + d.z + "/" + d.x + "/" + d.y + ".png";
}
}
else if (_ == "Stamen_TonerLite") {
a = "Map tiles by Stamen Design, under CC BY 3.0. Data by OpenStreetMap, under ODbL."
source = function(d) {
return "https://stamen-tiles.a.ssl.fastly.net/toner-lite/" + d.z + "/" + d.x + "/" + d.y + ".png";
}
}
else if (_ == "Stamen_Terrain") {
a = "Map tiles by Stamen Design, under CC BY 3.0. Data by OpenStreetMap, under ODbL."
source = function(d) {
return "https://stamen-tiles.a.ssl.fastly.net/terrain/" + d.z + "/" + d.x + "/" + d.y + ".png";
}
}
else if (_ == "Stamen_TerrainBackground") {
a = "Map tiles by Stamen Design, under CC BY 3.0. Data by OpenStreetMap, under ODbL."
source = function(d) {
return "https://stamen-tiles.a.ssl.fastly.net/terrain-background/" + d.z + "/" + d.x + "/" + d.y + ".png";
}
}
else if (_ == "Stamen_TerrainLines") {
a = "Map tiles by Stamen Design, under CC BY 3.0. Data by OpenStreetMap, under ODbL."
source = function(d) {
return "https://stamen-tiles.a.ssl.fastly.net/terrain-lines/" + d.z + "/" + d.x + "/" + d.y + ".png";
}
}
else if (_ == "Stamen_Watercolor") {
a = "Map tiles by Stamen Design, under CC BY 3.0. Data by OpenStreetMap, under CC BY SA."
source = function(d) {
return "https://stamen-tiles.a.ssl.fastly.net/watercolor/" + d.z + "/" + d.x + "/" + d.y + ".png";
}
}
else {
// Default:
console.log("Unknown Tileset, using OSM");
a = "Tiles © OpenStreetMap contributors";
source = function(d) {
return "http://" + "abc"[d.y % 3] + ".tile.openstreetmap.org/" + d.z + "/" + d.x + "/" + d.y + ".png";
}
}
return geoTile;
}
return geoTile;
}
exports.geoSlippy = geoTile;
Object.defineProperty(exports, '__esModule', { value: true });
}));
<!DOCTYPE html>
<meta charset="utf-8">
<canvas width="960" height="960"></canvas>
<script src="https://d3js.org/d3.v4.min.js"></script>
<script src="d3-slippy.js"></script>
<script>
// Canvas:
var canvas = d3.select("canvas");
var context = canvas.node().getContext("2d");
// Set up a slippy projection
var slippy = d3.geoSlippy()
.tileSet("")
.size(canvas);
// Set up a path generator
var path = d3.geoPath().projection(slippy.projection());
// Set zoom properties
var zoom = d3.zoom()
.on("zoom",zoomed)
.translateExtent(slippy.zoomTranslateConstrain())
.scaleExtent([1,128]);
// Call zoom and set initial zoom
canvas
.call(zoom)
.call(zoom.transform, slippy.zoomIdentity());
// Apply zoom
function zoomed() {
// Update projection.
slippy.zoomTransform(d3.event.transform)
// Update raster tiles:
slippy.canvas(context);
}
</script>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment