Initial testing canvas for d3-slippy.
Last active
February 2, 2019 02:12
-
-
Save Andrew-Reid/bc9c9a6203982773346cdeff3d0d9b1d to your computer and use it in GitHub Desktop.
D3-slippy map with canvas tiles
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// 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 }); | |
})); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<!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