Skip to content

Instantly share code, notes, and snippets.

@Andrew-Reid
Last active February 5, 2019 22:03
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save Andrew-Reid/7f1d1ae9cb8b9673791d15253bec7069 to your computer and use it in GitHub Desktop.
Save Andrew-Reid/7f1d1ae9cb8b9673791d15253bec7069 to your computer and use it in GitHub Desktop.
d3 geographic tile sets
// 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;
var lim = 85.051133;
// Map Properties:
var w = 960;
var h = 500;
// Projection Values:
var pk = w/tau; // projection scale k - default reference for tile placement.
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
// Offsets for other projections:
var ox = 0;
var oy = 0;
// 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 ordering:
var xyz = true;
function geoTile(_) {
return p(_);
}
// Tile source & attribution
geoTile.xyz = function(_) {
return arguments.length ? (xyz = _, geoTile): xyz;
}
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";
// 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 arguments.length ? (p = _, geoTile) : p;
}
geoTile.attribution = function(_) {
return arguments.length ? (a = _, geoTile) : a;
}
geoTile.wrap = function(_) {
return arguments.length ? (wrap = _, geoTile) : wrap;
}
geoTile.limit = function(_) {
return arguments.length ? (lim = _, geoTile) : lim;
}
// 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 ? (/*ox = ox/pk*_, oy = oy/pk*_, tx = tx/pk*_, ty = ty/pk*_,*/ pk = _, p.scale(pk), geoTile) : pk; // Scale modifies translate & offset.
}
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";
}
geoTile.offset = function(_) {
return arguments.length ? (ox = _[0], oy = _[1], geoTile): [ox,oy];
}
// 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] - oy * tk * pk/w*tau;
var x0 = p([-180,lim])[0] - ox * tk * pk/w*tau;
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;
}
if(Math.pow(z,2) > j && Math.pow(z,2) > x) set.push({x:x,y:j,z:Math.round(z),tx:i,ty:j, id:i+"-"+j+"-"+z})
}
}
if(!xyz) {
set.forEach(function(d) {
d.y = (Math.pow(2, d.z) - d.y - 1)
})
}
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(_) {
var m = function() {
ox = 0;
oy = 0;
p = d3.geoMercator().scale(pk).translate([0,0]);
lim = 85.05113;
xyz = true;
}
// arctic
var a0 = function(lon) {
lim = 90;
p = d3.geoAzimuthalEqualArea().rotate([lon,-90])
ox = w/2 + (960-w)/2;
oy = h/2 + (960-h)/2;
xyz = true;
}
var sets = {
// CARTO DB //***********************************************************************
"CartoDB_Positron" : function() {
m(), a = "© OpenStreetMap © CartoDB";
source = function(d) {
return "https://cartodb-basemaps-b.global.ssl.fastly.net/light_all/"+d.z+"/"+d.x+"/"+d.y+".png";
}
},
"CartoDB_PositronNoLabels" : function() {
m(), a = "© OpenStreetMap © CartoDB";
source = function(d) {
return "https://cartodb-basemaps-b.global.ssl.fastly.net/light_nolabels/"+d.z+"/"+d.x+"/"+d.y+".png";
}
},
"CartoDB_PositronOnlyLabels" : function() {
m(), 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";
}
},
"CartoDB_DarkMatter" : function() {
m(), a = "© OpenStreetMap © CartoDB";
source = function(d) {
return "https://cartodb-basemaps-b.global.ssl.fastly.net/dark_all/"+d.z+"/"+d.x+"/"+d.y+".png";
}
},
"CartoDB_DarkMatterNoLabels" : function() {
m(), a = "© OpenStreetMap © CartoDB";
source = function(d) {
return "https://cartodb-basemaps-b.global.ssl.fastly.net/dark_nolabels/"+d.z+"/"+d.x+"/"+d.y+".png";
}
},
"CartoDB_DarkMatterOnlyLabels" : function() {
m(), 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";
}
},
"CartoDB_Voyager" : function() {
m(), 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 //***********************************************************************
"ESRI_WorldTerrain" : function() {
m(), 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";
}
},
"ESRI_WorldShadedRelief" : function() {
m(), 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";
}
},
"ESRI_WorldPhysical" : function() {
m(), 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";
}
},
"ESRI_WorldStreetMap" : function() {
m(), 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";
}
},
"ESRI_WorldTopoMap": function() {
m(), 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";
}
},
"ESRI_WorldImagery": function() {
m(), 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";
}
},
"ESRI_OceanBasemap": function() {
m(), 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";
}
},
"ESRI_NGWorld" : function() {
m(), 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";
}
},
"ESRI_Gray": function() {
m(), 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";
}
},
// Open Street Map // *********************************************************************
"OSM_Topo" : function() {
m(), a = "Tiles © OpenStreetMap contributors";
source = function(d) {
return "https://tile.opentopomap.org/"+d.z+"/"+d.x+"/"+d.y+".png";
}
},
"OSM" : function() {
m(), a = "Tiles © OpenStreetMap contributors";
source = function(d) {
return "https://tile.opentopomap.org/"+d.z+"/"+d.x+"/"+d.y+".png";
}
},
// STAMEN // **************************************************************************
"Stamen_Toner" : function() {
m(), 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";
}
},
"Stamen_TonerBackground": function() {
m(), 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";
}
},
"Stamen_TonerLines": function() {
m(), 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";
}
},
"Stamen_TonerLite": function() {
m(), 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";
}
},
"Stamen_Terrain" : function() {
m(), 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";
}
},
"Stamen_TerrainBackground" : function() {
m(), 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";
}
},
"Stamen_TerrainLines": function() {
m(), 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";
}
},
"Stamen_Watercolor": function() {
m(); 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";
}
},
// Arctic Connect
"ArcticConnect_180": function() {
a0(180); a = "Map © ArcticConnect. Data © OpenStreetMap contributors",
source = function(d) {
return "http://a.tiles.arcticconnect.ca/osm_3571/"+d.z+"/"+d.x+"/"+d.y+".png";
}
},
"ArcticConnect_150w": function() {
a0(150); a = "Map © ArcticConnect. Data © OpenStreetMap contributors",
source = function(d) {
return "http://a.tiles.arcticconnect.ca/osm_3572/"+d.z+"/"+d.x+"/"+d.y+".png";
}
},
"ArcticConnect_100w": function() {
a0(100); a = "Map © ArcticConnect. Data © OpenStreetMap contributors",
source = function(d) {
return "http://a.tiles.arcticconnect.ca/osm_3573/"+d.z+"/"+d.x+"/"+d.y+".png";
}
},
"ArcticConnect_40w": function() {
a0(40); a = "Map © ArcticConnect. Data © OpenStreetMap contributors",
source = function(d) {
return "http://a.tiles.arcticconnect.ca/osm_3574/"+d.z+"/"+d.x+"/"+d.y+".png";
}
},
"ArcticConnect_10e": function() {
a0(-10); a = "Map © ArcticConnect. Data © OpenStreetMap contributors",
source = function(d) {
return "http://a.tiles.arcticconnect.ca/osm_3575/"+d.z+"/"+d.x+"/"+d.y+".png";
}
},
"ArcticConnect_90e": function() {
a0(-90); a = "Map © ArcticConnect. Data © OpenStreetMap contributors",
source = function(d) {
return "http://a.tiles.arcticconnect.ca/osm_3576/"+d.z+"/"+d.x+"/"+d.y+".png";
}
}
} // close sets.
if (_ == undefined) {
return Object.keys(sets);
}
else if(typeof _ == "function") {
source = _; return geoTile;
}
else if (sets[_]) {
sets[_](); return geoTile;
}
else {
console.log("Tileset not recognized, using OSM");
sets.OSM();
return geoTile;
}
return geoTile;
}
return geoTile;
}
exports.geoSlippy = geoTile;
Object.defineProperty(exports, '__esModule', { value: true });
}));
Further refinement of my [d3-slippy module](https://github.com/Andrew-Reid/d3-slippy/blob/master/README.md) to display geographic tiles/web map tiles.
This block runs through all the built in tilesets that can be accessed by string. I'll be changing how the tilesets are represented, but for the moment this serves as a test and demonstration of the various tilesets out there. There appear to be a few issues with some of the tilesets at certain zoom levels in certain areas, still a few kinks to sort out.
I'm not quite happy with the module's structure yet, but getting closer. Key challenges are implmenting clean non-cylindrical tile sets, this is causing some grief in terms of positioning.
<!DOCTYPE html>
<meta charset="utf-8">
<svg width="960" height="500"></svg>
<script src="https://d3js.org/d3.v4.js"></script>
<script src="https://unpkg.com/topojson-client@3"></script>
<script src="d3-slippy.js"></script>
<script>
d3.json("https://unpkg.com/world-atlas@1/world/110m.json", function(error, world) {
if (error) throw error;
// svg
var svg = d3.select("svg");
// Get list of tilesets:
var tilesets = d3.geoSlippy().tileSet();
// Drop down menu for tilesets:
var select = d3.select("body")
.append("select")
.attr("style","position: absolute; top: 0; left: 0; z-index: 0");
var options = select.selectAll("option")
.data(tilesets)
.enter()
.append("option")
.text(function(d) { return d; })
.attr("style","position: absolute; top: 10px; left 10px; z-index: 999");
// Set up slippy map initially:
var slippy = d3.geoSlippy()
.size(svg)
.tileSet("CartoDB_Positron")
.wrap(true);
// Set up zoom:
var zoom = d3.zoom()
.on("zoom",zoomed)
// Create g elements for tiles and vectors:
var tiles = svg.append("g");
var vector = svg.append("g");
// Append attribution:
var text = svg.append("text")
.attr("x", 10)
.attr("y", 490);
// Add features:
var path = d3.geoPath().projection(slippy.projection());
var features = vector.selectAll("path")
.data(topojson.feature(world,world.objects.land).features)
.enter()
.append("path")
.attr("fill","none")
.attr("stroke","black")
.attr("stroke-width",1)
svg
.call(zoom)
.call(zoom.transform, slippy.zoomIdentity());
// Apply zoom
function zoomed() {
// Update projection.
slippy.zoomTransform(d3.event.transform)
// Update raster tiles:
tiles.call(slippy.tile);
path = d3.geoPath().projection(slippy.projection());
features.attr("d",path);
text.text(slippy.attribution());
}
// Cycle through tilesets:
setInterval(showcase, 16000);
var index = 0;
function showcase( ) {
index++;
var tileset = options
.property("selected",false)
.filter(function(d,i) { return i == index%tilesets.length })
.property("selected",true)
.text();
slippy.tileSet(tileset);
svg.call(zoom.transform, slippy.zoomIdentity());
}
select.on("change", function(d) {
// assign new tileset and redraw:
slippy.tileSet(this.value);
svg.call(zoom.transform, slippy.zoomIdentity());
})
});
</script>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment