Skip to content

Instantly share code, notes, and snippets.

@ZJONSSON
Created October 5, 2011 00:20
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 ZJONSSON/1263239 to your computer and use it in GitHub Desktop.
Save ZJONSSON/1263239 to your computer and use it in GitHub Desktop.
SVG slippy map using Viewbox with D3

(hold shift when scrolling the mouse-wheel for now)

This is a simple slippy map that keeps the absolute coordinates of all tiles/location static, but moves the SVG viewbox as the user pans and zooms around. We can then draw SVG objects directly on the coordinate system and do not have to update location when the viewBox is changed.

There are two components to this, both standalone.

  1. svg_interact: provides pan and zoom to any svg-object by moving the viewBox
  2. svg_map: a tiler that watches the viewbox location and size at regular intervals and fetches the appropriate cloudmake tiles to cover the viewbox with some extra padding. This tiler uses the .data() method to enter and exit tiles. Another alternative might be to keep an array and push old items out as new come in. We need to discard old tiles as number of visible tiles affects performance. The graphic contents are however automatically cached by the browser.

I would really appreciate helpful hints on making this better and more d3 compatible (i.e. using pv.behavior to get better mouse functions)

<!DOCTYPE html>
<html>
<head>
<script type="text/javascript" src="http://mbostock.github.com/d3/d3.js"></script>
<script type="text/javascript" src="svg_interact.js"></script>
<script type="text/javascript" src="svg_map.js"></script>
</head>
<body></body>
<script type="text/javascript">
svg=d3.select("body").append("svg:svg").attr("height","900").attr("width","100%")
svg.call(svg_interact,{zoom_speed:1.15});
svg.call(svg_map,{api_key:"3eb45b95929d472d8fe4a2a5dafbd314"}); // Please get API key at cloudmake.com
</script>
</html>
function svg_interact(svg,p) {
return new svg_interactObj(svg,p);
}
function svg_interactObj(svg,p) {
var p = p ? p : {},
zoom_speed = p.zoom_speed ? p.zoom_speed : 1.15,
viewBox = svg[0][0].viewBox.baseVal;
panning = null,
current_mouse = null;
/* Panning moves the viewbox */
function mousemove(){
current_mouse = d3.svg.mouse(this);
if (panning) {
viewBox.x +=(panning[0] -current_mouse[0]);
viewBox.y += (panning[1] - current_mouse[1]);
}
};
svg.on("mousemove",mousemove)
svg.on("mousedown", function() { panning = d3.svg.mouse(this)});
d3.select(window).on("mouseup",function () { panning = null;})
svg[0][0].ondragstart = function() { return false } // Firefox fix
/* Zoom with mousewheel - keeping mouse position in same location*/
function wheel(event) {
var delta = 0;
if (!event) event = window.event;
if (event.wheelDelta) {
delta = event.wheelDelta/120;
} else if (event.detail) {
delta = -event.detail/3;
}
move = (delta<0) ? -delta * zoom_speed : 1/(delta*zoom_speed);
viewBox.x=(current_mouse[0]-(current_mouse[0]-viewBox.x)*move);
viewBox.y=(current_mouse[1]-(current_mouse[1]-viewBox.y)*move);
viewBox.height = viewBox.height * move;
viewBox.width = viewBox.width * move;
};
svg[0][0].addEventListener('DOMMouseScroll', wheel, false);
svg[0][0].onmousewheel = wheel;
document.onmousewheel = function () { return null};
return svg;
}
function lng2map(lon) { return (((lon+180)/360*Math.pow(2,0))); }
function lat2map(lat) { return (((1-Math.log(Math.tan(lat*Math.PI/180) + 1/Math.cos(lat*Math.PI/180))/Math.PI)/2 *Math.pow(2,0))); }
function svg_map(svg,p) { return new svg_map_obj(svg,p) }
function svg_map_obj(svg,p) {
var p = p ? p : {},
x0 = p.x0 ? p.x0 : 0.1286, // default x start location [0,1]
y0 = p.y0 ? p.y0 : 0.33066, // default y start location [0,1]
zoom = p.zoom ? p.zoom : 6, // default zoom at 6
update_delay = p.update_freq ? p.update_freq : 50, // How fast we look for new tiles
zoom_delay = p.zoom_delay ? p.zoom_delay : 1000, // How long we keep old tiles when zooming
tiles = [], keepTiles = [], // variables holding the tile ids
mapLayer = svg.append("svg:g").attr("class","mapLayer"),
viewBox = svg[0][0].viewBox.baseVal;
function PixelWidthSVG() { return svg[0][0].clientWidth ? svg[0][0].clientWidth : svg[0][0].parentNode.clientWidth;}
function PixelHeightSVG() { return svg[0][0].clientHeight ? svg[0][0].clientHeight : svg[0][0].parentNode.clientHeight;}
function resize() {
new_zoom = Math.floor(checkZoom());
viewBox.width = PixelWidthSVG() / (Math.pow(2,new_zoom)*256);
viewBox.height = PixelHeightSVG() / (Math.pow(2,new_zoom)*256);
loadTiles();
}
checkZoom = function() {
return Math.min(Math.max(Math.log(PixelWidthSVG()/(viewBox.width*256))/Math.log(2),0),18);
}
loadTiles = function() {
new_zoom = Math.floor(checkZoom());
if (zoom != new_zoom) {
zoom = new_zoom;
if (keepTiles.length == 0) {
keepTiles = tiles.slice();
setTimeout(function (){ keepTiles = []},1000)
}
}
no_x = Math.floor(PixelWidthSVG() / 256)+3;
no_y = Math.floor(PixelHeightSVG() / 256)+3;
firstX = Math.max(Math.floor(viewBox.x * Math.pow(2,zoom)-1),0);
firstY = Math.max(Math.floor(viewBox.y * Math.pow(2,zoom)-1),0);
tiles = [];
for (x=Math.max(firstX,0); x<Math.min(firstX+no_x,Math.pow(2,zoom));x++) {
for (y=Math.max(firstY,0); y <Math.min(firstY+no_y,Math.pow(2,zoom));y++) {
tiles.push({x:x,y:y,zoom:zoom});
};
};
images = mapLayer.selectAll("image").data(tiles.concat(keepTiles),function(d) { return d.zoom+"/"+d.x+"/"+d.y;});
images.enter()
.append("svg:image")
.attr("x",function(d) { return (d.x) / Math.pow(2,zoom);})
.attr("y",function(d) { return (d.y)/ Math.pow(2,zoom);})
.attr("height",1/Math.pow(2,zoom))
.attr("width",1/Math.pow(2,zoom))
.property("shape-rendering","crispEdges")
.attr("xlink:href",function(d) {
return "http://b.tile.cloudmade.com/"+p.api_key+"/1/256/"+zoom+"/"+d.x+"/"+d.y+".png";})
images.exit().remove();
}
svg.attr("viewBox",x0+" "+y0+" "+"0.1 0.1")
.attr("preserveAspectRatio", "xMinYMin meet"); // Ensure a viewbox exists with fixed aspect ratio
resize();
setInterval(loadTiles,update_delay); // Keep looking for new tiles and discard old
svg.svg_map = {checkZoom:checkZoom,resize:resize,loadTiles:loadTiles};
return svg.map_data;
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment