Skip to content

Instantly share code, notes, and snippets.

@martgnz
Last active October 28, 2017 15:27
Show Gist options
  • Select an option

  • Save martgnz/e5c0387a5bb675b061a2c0a9f573f86a to your computer and use it in GitHub Desktop.

Select an option

Save martgnz/e5c0387a5bb675b061a2c0a9f573f86a to your computer and use it in GitHub Desktop.

Revisions

  1. martgnz revised this gist Oct 28, 2017. 5 changed files with 66 additions and 62 deletions.
    8 changes: 0 additions & 8 deletions d3.min.js
    0 additions, 8 deletions not shown because the diff is too large. Please use a local Git client to view these changes.
    117 changes: 66 additions & 51 deletions index.html
    Original file line number Diff line number Diff line change
    @@ -1,59 +1,74 @@
    <!DOCTYPE html>
    <meta charset="utf-8" />
    <body>
    <script src="d3.min.js"></script>
    <script src="topojson.min.js"></script>
    <script src="rbush.min.js"></script>
    <script src="spam.min.js"></script>

    <script src="https://d3js.org/d3.v3.min.js" charset="utf-8"></script>
    <script src="https://d3js.org/topojson.v1.min.js"></script>
    <script src="https://unpkg.com/rbush@1.4.3/rbush.js"></script>
    <script src="https://unpkg.com/spamjs@1.1.0/spam.min.js"></script>
    <script type='text/javascript'>
    d3.tsv("antarctic-bases.tsv", function(error, antarctica) {
    d3.json("antarctica.json", function(error, d) {
    topojson.presimplify(d)
    d3.json("antarctica.json", function(error, d) {
    topojson.presimplify(d);

    var map = new StaticCanvasMap({
    element: "body",
    width: 960,
    height: 900,
    projection: d3.geo
    .stereographic()
    .scale(1000)
    .translate([960 / 2, -650])
    .clipAngle(180 - 1e-4)
    .precision(0.1),
    data: [
    {
    features: topojson.feature(d, d.objects["antarctica"]),
    static: {
    paintfeature: function(parameters, d) {
    parameters.context.lineWidth = 0.5 / parameters.scale;
    parameters.context.strokeStyle = "rgb(170,170,170)";
    parameters.context.stroke();

    parameters.context.fillStyle = "rgba(51, 103, 153, 0.1)";
    parameters.context.fill();
    },
    postpaint: function(parameters, d) {
    antarctica
    .filter(function(d) {
    return d.countries === "Russia";
    })
    .forEach(function(d) {
    var projectedPoint = parameters.map
    .settings()
    .projection([+d.lng, +d.lat]);

    var map = new StaticCanvasMap({
    element: "body",
    width: 960,
    height: 900,
    projection: d3.geo.stereographic()
    .scale(1000)
    .translate([960 / 2, -650])
    .clipAngle(180 - 1e-4)
    .precision(.1),
    data: [
    {
    features: topojson.feature(d, d.objects["antarctica"]),
    static: {
    paintfeature: function(parameters, d) {
    parameters.context.lineWidth = 0.5 / parameters.scale
    parameters.context.strokeStyle = "rgb(170,170,170)"
    parameters.context.stroke()
    parameters.context.beginPath();
    parameters.context.textAlign = "center";
    parameters.context.font = "bold 12px sans-serif";
    parameters.context.fillStyle = "rgb(70,70,70)";

    parameters.context.fillStyle = "rgba(51, 103, 153, 0.1)"
    parameters.context.fill()
    },
    postpaint: function(parameters, d) {
    antarctica.filter(function(d) {
    return d.countries === "Russia"
    })
    .forEach(function(d) {
    var projectedPoint = parameters.map.settings().projection([+d.lng, +d.lat])

    parameters.context.beginPath()
    parameters.context.textAlign = "center"
    parameters.context.font = "bold 12px sans-serif"
    parameters.context.fillStyle = "rgb(70,70,70)"

    parameters.context.arc(projectedPoint[0], projectedPoint[1] / parameters.scale, 2 / parameters.scale, 0, 2 * Math.PI, true)
    parameters.context.fillText(d.name, projectedPoint[0], projectedPoint[1] - 5)
    parameters.context.fill()
    })
    }
    }
    }
    ]
    })
    map.init()
    })
    })
    parameters.context.arc(
    projectedPoint[0],
    projectedPoint[1] / parameters.scale,
    2 / parameters.scale,
    0,
    2 * Math.PI,
    true
    );
    parameters.context.fillText(
    d.name,
    projectedPoint[0],
    projectedPoint[1] - 5
    );
    parameters.context.fill();
    });
    }
    }
    }
    ]
    });
    map.init();
    });
    });
    </script>
    1 change: 0 additions & 1 deletion rbush.min.js
    Original file line number Diff line number Diff line change
    @@ -1 +0,0 @@
    (function(){"use strict";function rbush(maxEntries,format){if(!(this instanceof rbush))return new rbush(maxEntries,format);this._maxEntries=Math.max(4,maxEntries||9);this._minEntries=Math.max(2,Math.ceil(this._maxEntries*.4));if(format){this._initFormat(format)}this.clear()}rbush.prototype={all:function(){return this._all(this.data,[])},search:function(bbox){var node=this.data,result=[],toBBox=this.toBBox;if(!intersects(bbox,node.bbox))return result;var nodesToSearch=[],i,len,child,childBBox;while(node){for(i=0,len=node.children.length;i<len;i++){child=node.children[i];childBBox=node.leaf?toBBox(child):child.bbox;if(intersects(bbox,childBBox)){if(node.leaf)result.push(child);else if(contains(bbox,childBBox))this._all(child,result);else nodesToSearch.push(child)}}node=nodesToSearch.pop()}return result},collides:function(bbox){var node=this.data,toBBox=this.toBBox;if(!intersects(bbox,node.bbox))return false;var nodesToSearch=[],i,len,child,childBBox;while(node){for(i=0,len=node.children.length;i<len;i++){child=node.children[i];childBBox=node.leaf?toBBox(child):child.bbox;if(intersects(bbox,childBBox)){if(node.leaf||contains(bbox,childBBox))return true;nodesToSearch.push(child)}}node=nodesToSearch.pop()}return false},load:function(data){if(!(data&&data.length))return this;if(data.length<this._minEntries){for(var i=0,len=data.length;i<len;i++){this.insert(data[i])}return this}var node=this._build(data.slice(),0,data.length-1,0);if(!this.data.children.length){this.data=node}else if(this.data.height===node.height){this._splitRoot(this.data,node)}else{if(this.data.height<node.height){var tmpNode=this.data;this.data=node;node=tmpNode}this._insert(node,this.data.height-node.height-1,true)}return this},insert:function(item){if(item)this._insert(item,this.data.height-1);return this},clear:function(){this.data={children:[],height:1,bbox:empty(),leaf:true};return this},remove:function(item){if(!item)return this;var node=this.data,bbox=this.toBBox(item),path=[],indexes=[],i,parent,index,goingUp;while(node||path.length){if(!node){node=path.pop();parent=path[path.length-1];i=indexes.pop();goingUp=true}if(node.leaf){index=node.children.indexOf(item);if(index!==-1){node.children.splice(index,1);path.push(node);this._condense(path);return this}}if(!goingUp&&!node.leaf&&contains(node.bbox,bbox)){path.push(node);indexes.push(i);i=0;parent=node;node=node.children[0]}else if(parent){i++;node=parent.children[i];goingUp=false}else node=null}return this},toBBox:function(item){return item},compareMinX:function(a,b){return a[0]-b[0]},compareMinY:function(a,b){return a[1]-b[1]},toJSON:function(){return this.data},fromJSON:function(data){this.data=data;return this},_all:function(node,result){var nodesToSearch=[];while(node){if(node.leaf)result.push.apply(result,node.children);else nodesToSearch.push.apply(nodesToSearch,node.children);node=nodesToSearch.pop()}return result},_build:function(items,left,right,height){var N=right-left+1,M=this._maxEntries,node;if(N<=M){node={children:items.slice(left,right+1),height:1,bbox:null,leaf:true};calcBBox(node,this.toBBox);return node}if(!height){height=Math.ceil(Math.log(N)/Math.log(M));M=Math.ceil(N/Math.pow(M,height-1))}node={children:[],height:height,bbox:null,leaf:false};var N2=Math.ceil(N/M),N1=N2*Math.ceil(Math.sqrt(M)),i,j,right2,right3;multiSelect(items,left,right,N1,this.compareMinX);for(i=left;i<=right;i+=N1){right2=Math.min(i+N1-1,right);multiSelect(items,i,right2,N2,this.compareMinY);for(j=i;j<=right2;j+=N2){right3=Math.min(j+N2-1,right2);node.children.push(this._build(items,j,right3,height-1))}}calcBBox(node,this.toBBox);return node},_chooseSubtree:function(bbox,node,level,path){var i,len,child,targetNode,area,enlargement,minArea,minEnlargement;while(true){path.push(node);if(node.leaf||path.length-1===level)break;minArea=minEnlargement=Infinity;for(i=0,len=node.children.length;i<len;i++){child=node.children[i];area=bboxArea(child.bbox);enlargement=enlargedArea(bbox,child.bbox)-area;if(enlargement<minEnlargement){minEnlargement=enlargement;minArea=area<minArea?area:minArea;targetNode=child}else if(enlargement===minEnlargement){if(area<minArea){minArea=area;targetNode=child}}}node=targetNode}return node},_insert:function(item,level,isNode){var toBBox=this.toBBox,bbox=isNode?item.bbox:toBBox(item),insertPath=[];var node=this._chooseSubtree(bbox,this.data,level,insertPath);node.children.push(item);extend(node.bbox,bbox);while(level>=0){if(insertPath[level].children.length>this._maxEntries){this._split(insertPath,level);level--}else break}this._adjustParentBBoxes(bbox,insertPath,level)},_split:function(insertPath,level){var node=insertPath[level],M=node.children.length,m=this._minEntries;this._chooseSplitAxis(node,m,M);var splitIndex=this._chooseSplitIndex(node,m,M);var newNode={children:node.children.splice(splitIndex,node.children.length-splitIndex),height:node.height,bbox:null,leaf:false};if(node.leaf)newNode.leaf=true;calcBBox(node,this.toBBox);calcBBox(newNode,this.toBBox);if(level)insertPath[level-1].children.push(newNode);else this._splitRoot(node,newNode)},_splitRoot:function(node,newNode){this.data={children:[node,newNode],height:node.height+1,bbox:null,leaf:false};calcBBox(this.data,this.toBBox)},_chooseSplitIndex:function(node,m,M){var i,bbox1,bbox2,overlap,area,minOverlap,minArea,index;minOverlap=minArea=Infinity;for(i=m;i<=M-m;i++){bbox1=distBBox(node,0,i,this.toBBox);bbox2=distBBox(node,i,M,this.toBBox);overlap=intersectionArea(bbox1,bbox2);area=bboxArea(bbox1)+bboxArea(bbox2);if(overlap<minOverlap){minOverlap=overlap;index=i;minArea=area<minArea?area:minArea}else if(overlap===minOverlap){if(area<minArea){minArea=area;index=i}}}return index},_chooseSplitAxis:function(node,m,M){var compareMinX=node.leaf?this.compareMinX:compareNodeMinX,compareMinY=node.leaf?this.compareMinY:compareNodeMinY,xMargin=this._allDistMargin(node,m,M,compareMinX),yMargin=this._allDistMargin(node,m,M,compareMinY);if(xMargin<yMargin)node.children.sort(compareMinX)},_allDistMargin:function(node,m,M,compare){node.children.sort(compare);var toBBox=this.toBBox,leftBBox=distBBox(node,0,m,toBBox),rightBBox=distBBox(node,M-m,M,toBBox),margin=bboxMargin(leftBBox)+bboxMargin(rightBBox),i,child;for(i=m;i<M-m;i++){child=node.children[i];extend(leftBBox,node.leaf?toBBox(child):child.bbox);margin+=bboxMargin(leftBBox)}for(i=M-m-1;i>=m;i--){child=node.children[i];extend(rightBBox,node.leaf?toBBox(child):child.bbox);margin+=bboxMargin(rightBBox)}return margin},_adjustParentBBoxes:function(bbox,path,level){for(var i=level;i>=0;i--){extend(path[i].bbox,bbox)}},_condense:function(path){for(var i=path.length-1,siblings;i>=0;i--){if(path[i].children.length===0){if(i>0){siblings=path[i-1].children;siblings.splice(siblings.indexOf(path[i]),1)}else this.clear()}else calcBBox(path[i],this.toBBox)}},_initFormat:function(format){var compareArr=["return a"," - b",";"];this.compareMinX=new Function("a","b",compareArr.join(format[0]));this.compareMinY=new Function("a","b",compareArr.join(format[1]));this.toBBox=new Function("a","return [a"+format.join(", a")+"];")}};function calcBBox(node,toBBox){node.bbox=distBBox(node,0,node.children.length,toBBox)}function distBBox(node,k,p,toBBox){var bbox=empty();for(var i=k,child;i<p;i++){child=node.children[i];extend(bbox,node.leaf?toBBox(child):child.bbox)}return bbox}function empty(){return[Infinity,Infinity,-Infinity,-Infinity]}function extend(a,b){a[0]=Math.min(a[0],b[0]);a[1]=Math.min(a[1],b[1]);a[2]=Math.max(a[2],b[2]);a[3]=Math.max(a[3],b[3]);return a}function compareNodeMinX(a,b){return a.bbox[0]-b.bbox[0]}function compareNodeMinY(a,b){return a.bbox[1]-b.bbox[1]}function bboxArea(a){return(a[2]-a[0])*(a[3]-a[1])}function bboxMargin(a){return a[2]-a[0]+(a[3]-a[1])}function enlargedArea(a,b){return(Math.max(b[2],a[2])-Math.min(b[0],a[0]))*(Math.max(b[3],a[3])-Math.min(b[1],a[1]))}function intersectionArea(a,b){var minX=Math.max(a[0],b[0]),minY=Math.max(a[1],b[1]),maxX=Math.min(a[2],b[2]),maxY=Math.min(a[3],b[3]);return Math.max(0,maxX-minX)*Math.max(0,maxY-minY)}function contains(a,b){return a[0]<=b[0]&&a[1]<=b[1]&&b[2]<=a[2]&&b[3]<=a[3]}function intersects(a,b){return b[0]<=a[2]&&b[1]<=a[3]&&b[2]>=a[0]&&b[3]>=a[1]}function multiSelect(arr,left,right,n,compare){var stack=[left,right],mid;while(stack.length){right=stack.pop();left=stack.pop();if(right-left<=n)continue;mid=left+Math.ceil((right-left)/n/2)*n;select(arr,left,right,mid,compare);stack.push(left,mid,mid,right)}}function select(arr,left,right,k,compare){var n,i,z,s,sd,newLeft,newRight,t,j;while(right>left){if(right-left>600){n=right-left+1;i=k-left+1;z=Math.log(n);s=.5*Math.exp(2*z/3);sd=.5*Math.sqrt(z*s*(n-s)/n)*(i-n/2<0?-1:1);newLeft=Math.max(left,Math.floor(k-i*s/n+sd));newRight=Math.min(right,Math.floor(k+(n-i)*s/n+sd));select(arr,newLeft,newRight,k,compare)}t=arr[k];i=left;j=right;swap(arr,left,k);if(compare(arr[right],t)>0)swap(arr,left,right);while(i<j){swap(arr,i,j);i++;j--;while(compare(arr[i],t)<0)i++;while(compare(arr[j],t)>0)j--}if(compare(arr[left],t)===0)swap(arr,left,j);else{j++;swap(arr,j,right)}if(j<=k)left=j+1;if(k<=j)right=j-1}}function swap(arr,i,j){var tmp=arr[i];arr[i]=arr[j];arr[j]=tmp}if(typeof define==="function"&&define.amd)define("rbush",function(){return rbush});else if(typeof module!=="undefined")module.exports=rbush;else if(typeof self!=="undefined")self.rbush=rbush;else window.rbush=rbush})();
    1 change: 0 additions & 1 deletion spam.min.js
    Original file line number Diff line number Diff line change
    @@ -1 +0,0 @@
    var StaticCanvasMap;var ZoomableCanvasMap;!function(){"use strict";function inside(pt,polygon){var polys=polygon.geometry.coordinates;if(polygon.geometry.type==="Polygon")polys=[polys];var insidePoly=false;var i=0;while(i<polys.length&&!insidePoly){if(inRing(pt,polys[i][0])){var inHole=false;var k=1;while(k<polys[i].length&&!inHole){if(inRing(pt,polys[i][k])){inHole=true}k++}if(!inHole)insidePoly=true}i++}return insidePoly}function inRing(pt,ring){var isInside=false;for(var i=0,j=ring.length-1;i<ring.length;j=i++){var xi=ring[i][0],yi=ring[i][1];var xj=ring[j][0],yj=ring[j][1];var intersect=yi>pt[1]!==yj>pt[1]&&pt[0]<(xj-xi)*(pt[1]-yi)/(yj-yi)+xi;if(intersect)isInside=!isInside}return isInside}function maxBounds(one,two){var bounds=two;if(one[0][0]<two[0][0])bounds[0][0]=one[0][0];if(one[0][1]<two[0][1])bounds[0][1]=one[0][1];if(one[1][0]>two[1][0])bounds[1][0]=one[1][0];if(one[1][1]>two[1][1])bounds[1][1]=one[1][1];return bounds}function createRTree(element,dataPath){element.lookupTree=rbush(4);var elements=[];for(var j in element.features.features){var bounds=dataPath.bounds(element.features.features[j]);elements.push([bounds[0][0].toFixed(0),bounds[0][1].toFixed(0),Math.ceil(bounds[1][0]),Math.ceil(bounds[1][1]),element.features.features[j]])}element.lookupTree.load(elements)}function paintFeature(element,feature,parameters){parameters.context.beginPath();parameters.path(feature);element.static.paintfeature(parameters,feature)}function paintBackgroundElement(element,parameters){if(!element.static)return;if(element.static.prepaint)element.static.prepaint(parameters);if(element.static.paintfeature){var lookup=element.lookupTree.search([parameters.translate[0],parameters.translate[1],parameters.width/parameters.scale-parameters.translate[0],parameters.height/parameters.scale-parameters.translate[1]]);for(var j in lookup){paintFeature(element,lookup[j][4],parameters)}}if(element.static.postpaint)element.static.postpaint(parameters)}function PartialPainter(data,parameters){var index=0,j=0,element=null,currentLookup=[];this.hasNext=function(){return index<=data.length&&j<currentLookup.length};this.renderNext=function(){if(index>=data.length&&j>=currentLookup.length)return;var start=performance.now();if(!element||j>=currentLookup.length){while(index<data.length&&!data[index].static){index++}if(index>=data.length)return;element=data[index];if(element.static.prepaint)element.static.prepaint(parameters);currentLookup=element.lookupTree.search([-parameters.translate[0],-parameters.translate[1],parameters.width/parameters.scale-parameters.translate[0],parameters.height/parameters.scale-parameters.translate[1]]);j=0;++index}if(element.static.paintfeature){for(;j!=currentLookup.length;++j){var feature=currentLookup[j][4];paintFeature(element,feature,parameters);if(performance.now()-start>10)break}}else{j=currentLookup.length}if(j==currentLookup.length&&element.static.postpaint){element.static.postpaint(parameters)}};this.finish=function(){if(index>=data.length&&j>=currentLookup.length)return;if(j<currentLookup.length)index--;for(;index!=data.length;++index){if(j>=currentLookup.length){while(!data[index].static&&index<data.length){index++}if(index>=data.length)return;element=data[index];if(element.static.prepaint)element.static.prepaint(parameters);currentLookup=element.lookupTree.search([-parameters.translate[0],-parameters.translate[1],parameters.width/parameters.scale-parameters.translate[0],parameters.height/parameters.scale-parameters.translate[1]]);j=0}if(element.static.paintfeature){for(;j!=currentLookup.length;++j){var feature=currentLookup[j][4];paintFeature(element,feature,parameters)}}if(element.static.postpaint)element.static.postpaint(parameters)}}}function translatePoint(point,scale,translate){return[point[0]/scale-translate[0],point[1]/scale-translate[1]]}function extend(extension,obj){var newObj={};for(var elem in obj){newObj[elem]=obj[elem]}for(var elem in extension){if(!newObj.hasOwnProperty(elem))newObj[elem]=extension[elem]}return newObj}function CanvasMap(parameters){var settings=extend({width:d3.select(parameters.element).node().getBoundingClientRect().width,ratio:1,area:0,scale:1,translate:[0,0],background:null,backgroundScale:1,backgroundTranslate:[0,0],map:this},parameters),simplify=d3.geo.transform({point:function(x,y,z){if(!z||z>=settings.area){this.stream.point(x,y)}}}),canvas=null,context=null;if(!parameters.projection){var b=[[Infinity,Infinity],[-Infinity,-Infinity]];for(var i in settings.data){b=maxBounds(b,d3.geo.bounds(settings.data[i].features))}settings.projection=d3.geo.mercator().scale(1).center([(b[1][0]+b[0][0])/2,(b[1][1]+b[0][1])/2])}var dataPath=d3.geo.path().projection({stream:function(s){return simplify.stream(settings.projection.stream(s))}});var b=[[Infinity,Infinity],[-Infinity,-Infinity]];for(var i in settings.data){b=maxBounds(b,dataPath.bounds(settings.data[i].features))}var dx=b[1][0]-b[0][0],dy=b[1][1]-b[0][1];if(!parameters.projection){settings.height=settings.height||Math.ceil(dy*settings.width/dx);settings.projection.scale(.9*(settings.width/dx)).translate([settings.width/2,settings.height/2])}else if(!settings.height){settings.height=Math.ceil(dy*1/.9)}d3.select(settings.parameters).attr("height",settings.height);function init(){canvas=d3.select(settings.element).append("canvas");context=canvas.node().getContext("2d");var devicePixelRatio=window.devicePixelRatio||1,backingStoreRatio=context.webkitBackingStorePixelRatio||context.mozBackingStorePixelRatio||context.msBackingStorePixelRatio||context.oBackingStorePixelRatio||context.backingStorePixelRatio||1;settings.ratio=devicePixelRatio/backingStoreRatio;settings.area=1/settings.projection.scale()/settings.ratio/20;canvas.attr("width",settings.width*settings.ratio);canvas.attr("height",settings.height*settings.ratio);canvas.style("width",settings.width+"px");canvas.style("height",settings.height+"px");context.lineJoin="round";context.lineCap="round";dataPath.context(context);context.clearRect(0,0,settings.width*settings.ratio,settings.height*settings.ratio);context.save();context.scale(settings.ratio,settings.ratio);for(var i in settings.data){createRTree(settings.data[i],dataPath)}settings.background=new Image;settings.backgroundScale=settings.scale;settings.backgroundTranslate=settings.translate;var parameters={path:dataPath,context:context,scale:settings.scale,translate:settings.translate,width:settings.width,height:settings.height,map:settings.map};var callback=function(){for(var i in settings.data){var element=settings.data[i];if(element.dynamic&&element.dynamic.postpaint)element.dynamic.postpaint(parameters,null)}context.restore();canvas.on("click",click).on("mousemove",hover).on("mouseleave",hoverLeave)};for(var i in settings.data){var element=settings.data[i];if(element.dynamic&&element.dynamic.prepaint)element.dynamic.prepaint(parameters,element.hoverElement)}for(var i in settings.data){var element=settings.data[i];paintBackgroundElement(element,parameters)}settings.background.onload=callback;settings.background.src=canvas.node().toDataURL();this.init=function(){}}function paint(){context.save();context.scale(settings.scale*settings.ratio,settings.scale*settings.ratio);context.translate(settings.translate[0],settings.translate[1]);context.clearRect(-settings.translate[0],-settings.translate[1],settings.width*settings.ratio,settings.height*settings.ratio);context.rect(-settings.translate[0],-settings.translate[1],settings.width/settings.scale,settings.height/settings.scale);context.clip();var parameters={path:dataPath,context:dataPath.context(),scale:settings.scale,translate:settings.translate,width:settings.width,height:settings.height,map:settings.map};settings.area=1/settings.projection.scale()/settings.scale/settings.ratio/20;for(var i in settings.data){var element=settings.data[i];if(element.dynamic&&element.dynamic.prepaint)element.dynamic.prepaint(parameters,element.hoverElement)}context.drawImage(settings.background,0,0,settings.width*settings.ratio,settings.height*settings.ratio,-settings.backgroundTranslate[0],-settings.backgroundTranslate[1],settings.width/settings.backgroundScale,settings.height/settings.backgroundScale);for(var i in settings.data){var element=settings.data[i];if(element.dynamic&&element.dynamic.postpaint)element.dynamic.postpaint(parameters,element.hoverElement)}context.restore()}function click(){var point=translatePoint(d3.mouse(this),settings.scale,settings.translate);var parameters={path:dataPath,context:context,scale:settings.scale,translate:settings.translate,width:settings.width,height:settings.height,map:settings.map};for(var i in settings.data){var element=settings.data[i];if(!element.click)continue;var lookup=element.lookupTree.search([point[0],point[1],point[0],point[1]]);var isInside=false;for(var j in lookup){var feature=lookup[j][4];if(inside(settings.projection.invert(point),feature)){element.click(parameters,feature);isInside=true}}isInside||element.click(parameters,null)}}function hoverLeave(){for(var i in settings.data){var element=settings.data[i];element.hoverElement=false}paint()}function hover(){var point=translatePoint(d3.mouse(this),settings.scale,settings.translate),repaint=false;for(var i in settings.data){var element=settings.data[i];if(element.hoverElement&&inside(settings.projection.invert(point),element.hoverElement)){continue}element.hoverElement=false;var lookup=element.lookupTree.search([point[0],point[1],point[0],point[1]]);for(var j in lookup){var feature=lookup[j][4];if(inside(settings.projection.invert(point),feature)){element.hoverElement=feature;break}}repaint=true}repaint&&paint()}this.init=init;this.paint=paint;this.settings=function(){return settings}}StaticCanvasMap=function(parameters){var map=new CanvasMap(parameters);this.init=function(){map.init()};this.paint=function(){map.paint()}};var epsilon=.5;function nearEqual(a,b){return Math.abs(a-b)<epsilon}function ImageCache(parameters){var cache=[],settings=parameters;this.addImage=function(parameters){cache.push(parameters)};this.getImage=function(parameters){for(var i in cache){var element=cache[i];if(nearEqual(element.scale,parameters.scale)&&nearEqual(element.translate[0],parameters.translate[0])&&nearEqual(element.translate[1],parameters.translate[1]))return element}return null};this.getFittingImage=function(bbox){var currentImage=cache.length>0?cache[0]:null;for(var i in cache){var image=cache[i];var imageBB=[-image.translate[0],-image.translate[1],settings.width/image.scale-image.translate[0],settings.height/image.scale-image.translate[1]];if(imageBB[0]<=bbox[0]&&imageBB[1]<=bbox[1]&&imageBB[2]>=bbox[2]&&imageBB[3]>=bbox[3]&&(!currentImage||currentImage.scale<image.scale)){currentImage=image}}return currentImage}}ZoomableCanvasMap=function(parameters){var map=new CanvasMap(parameters),simplify=d3.geo.transform({point:function(x,y,z){if(z>=area)this.stream.point(x,y)}}),area=0,canvas=null,context=null,settings=map.settings(),dataPath=d3.geo.path().projection({stream:function(s){return simplify.stream(settings.projection.stream(s))}}),imageCache=new ImageCache({width:settings.width,height:settings.height});settings.map=this;settings.zoomScale=settings.zoomScale||.5;this.init=function(){map.init();canvas=d3.select(settings.element).append("canvas");context=canvas.node().getContext("2d");area=1/settings.projection.scale()/settings.ratio/20;canvas.attr("width",settings.width*settings.ratio);canvas.attr("height",settings.height*settings.ratio);canvas.style("width",settings.width+"px");canvas.style("height",settings.height+"px");canvas.style("display","none");context.lineJoin="round";context.lineCap="round";dataPath.context(context);imageCache.addImage({image:settings.background,scale:settings.scale,translate:settings.translate})};this.paint=function(){map.paint()};function scaleZoom(scale,translate){if(nearEqual(scale,settings.scale)&&nearEqual(translate[0],settings.translate[0])&&nearEqual(translate[1],settings.translate[1])){scale=1;translate=[0,0]}if(scale==1&&settings.scale==1&&!translate[0]&&!translate[1]&&!settings.translate[0]&&!settings.translate[1]){return}area=1/settings.projection.scale()/scale/settings.ratio/20;context.save();context.scale(scale*settings.ratio,scale*settings.ratio);context.translate(translate[0],translate[1]);context.clearRect(-translate[0],-translate[1],settings.width*settings.ratio,settings.height*settings.ratio);var parameters={path:dataPath,context:context,scale:scale,translate:translate,width:settings.width,height:settings.height,map:settings.map};var image=imageCache.getImage({scale:scale,translate:translate});if(!image){var background=new Image,partialPainter=new PartialPainter(settings.data,parameters)}var translatedOne=translatePoint([settings.width,settings.height],scale,translate),translatedTwo=translatePoint([settings.width,settings.height],settings.scale,settings.translate);var bbox=[Math.min(-translate[0],-settings.translate[0]),Math.min(-translate[1],-settings.translate[1]),Math.max(translatedOne[0],translatedTwo[0]),Math.max(translatedOne[1],translatedTwo[1])];var zoomImage=imageCache.getFittingImage(bbox);if(zoomImage){settings.background=zoomImage.image;settings.backgroundScale=zoomImage.scale;settings.backgroundTranslate=zoomImage.translate}d3.transition().duration(300).ease("linear").tween("zoom",function(){var i=d3.interpolateNumber(settings.scale,scale),oldTranslate=settings.translate,oldScale=settings.scale;return function(t){settings.scale=i(t);var newTranslate=[oldTranslate[0]+(translate[0]-oldTranslate[0])/(scale-oldScale)*(i(t)-oldScale)*scale/i(t),oldTranslate[1]+(translate[1]-oldTranslate[1])/(scale-oldScale)*(i(t)-oldScale)*scale/i(t)];settings.translate=newTranslate;map.paint();!image&&partialPainter.renderNext()}}).each("end",function(){settings.scale=scale;settings.translate=translate;if(image){context.restore();settings.background=image.image;settings.backgroundScale=image.scale;settings.backgroundTranslate=image.translate;map.paint()}else{partialPainter.finish();background.onload=function(){context.restore();imageCache.addImage({image:background,scale:scale,translate:translate});settings.background=background;settings.backgroundScale=scale;settings.backgroundTranslate=translate;map.paint()};background.src=canvas.node().toDataURL()}})}this.zoom=function(d){if(!d){scaleZoom.call(this,1,[0,0]);return}var bounds=dataPath.bounds(d),dx=bounds[1][0]-bounds[0][0],dy=bounds[1][1]-bounds[0][1],bx=(bounds[0][0]+bounds[1][0])/2,by=(bounds[0][1]+bounds[1][1])/2,scale=settings.zoomScale*Math.min(settings.width/dx,settings.height/dy),translate=[-bx+settings.width/scale/2,-by+settings.height/scale/2];scaleZoom.call(this,scale,translate)}}}();
    1 change: 0 additions & 1 deletion topojson.min.js
    Original file line number Diff line number Diff line change
    @@ -1 +0,0 @@
    !function(){var topojson={version:"1.6.19",mesh:function(topology){return object(topology,meshArcs.apply(this,arguments))},meshArcs:meshArcs,merge:function(topology){return object(topology,mergeArcs.apply(this,arguments))},mergeArcs:mergeArcs,feature:featureOrCollection,neighbors:neighbors,presimplify:presimplify};function stitchArcs(topology,arcs){var stitchedArcs={},fragmentByStart={},fragmentByEnd={},fragments=[],emptyIndex=-1;arcs.forEach(function(i,j){var arc=topology.arcs[i<0?~i:i],t;if(arc.length<3&&!arc[1][0]&&!arc[1][1]){t=arcs[++emptyIndex],arcs[emptyIndex]=i,arcs[j]=t}});arcs.forEach(function(i){var e=ends(i),start=e[0],end=e[1],f,g;if(f=fragmentByEnd[start]){delete fragmentByEnd[f.end];f.push(i);f.end=end;if(g=fragmentByStart[end]){delete fragmentByStart[g.start];var fg=g===f?f:f.concat(g);fragmentByStart[fg.start=f.start]=fragmentByEnd[fg.end=g.end]=fg}else{fragmentByStart[f.start]=fragmentByEnd[f.end]=f}}else if(f=fragmentByStart[end]){delete fragmentByStart[f.start];f.unshift(i);f.start=start;if(g=fragmentByEnd[start]){delete fragmentByEnd[g.end];var gf=g===f?f:g.concat(f);fragmentByStart[gf.start=g.start]=fragmentByEnd[gf.end=f.end]=gf}else{fragmentByStart[f.start]=fragmentByEnd[f.end]=f}}else{f=[i];fragmentByStart[f.start=start]=fragmentByEnd[f.end=end]=f}});function ends(i){var arc=topology.arcs[i<0?~i:i],p0=arc[0],p1;if(topology.transform)p1=[0,0],arc.forEach(function(dp){p1[0]+=dp[0],p1[1]+=dp[1]});else p1=arc[arc.length-1];return i<0?[p1,p0]:[p0,p1]}function flush(fragmentByEnd,fragmentByStart){for(var k in fragmentByEnd){var f=fragmentByEnd[k];delete fragmentByStart[f.start];delete f.start;delete f.end;f.forEach(function(i){stitchedArcs[i<0?~i:i]=1});fragments.push(f)}}flush(fragmentByEnd,fragmentByStart);flush(fragmentByStart,fragmentByEnd);arcs.forEach(function(i){if(!stitchedArcs[i<0?~i:i])fragments.push([i])});return fragments}function meshArcs(topology,o,filter){var arcs=[];if(arguments.length>1){var geomsByArc=[],geom;function arc(i){var j=i<0?~i:i;(geomsByArc[j]||(geomsByArc[j]=[])).push({i:i,g:geom})}function line(arcs){arcs.forEach(arc)}function polygon(arcs){arcs.forEach(line)}function geometry(o){if(o.type==="GeometryCollection")o.geometries.forEach(geometry);else if(o.type in geometryType)geom=o,geometryType[o.type](o.arcs)}var geometryType={LineString:line,MultiLineString:polygon,Polygon:polygon,MultiPolygon:function(arcs){arcs.forEach(polygon)}};geometry(o);geomsByArc.forEach(arguments.length<3?function(geoms){arcs.push(geoms[0].i)}:function(geoms){if(filter(geoms[0].g,geoms[geoms.length-1].g))arcs.push(geoms[0].i)})}else{for(var i=0,n=topology.arcs.length;i<n;++i)arcs.push(i)}return{type:"MultiLineString",arcs:stitchArcs(topology,arcs)}}function mergeArcs(topology,objects){var polygonsByArc={},polygons=[],components=[];objects.forEach(function(o){if(o.type==="Polygon")register(o.arcs);else if(o.type==="MultiPolygon")o.arcs.forEach(register)});function register(polygon){polygon.forEach(function(ring){ring.forEach(function(arc){(polygonsByArc[arc=arc<0?~arc:arc]||(polygonsByArc[arc]=[])).push(polygon)})});polygons.push(polygon)}function exterior(ring){return cartesianRingArea(object(topology,{type:"Polygon",arcs:[ring]}).coordinates[0])>0}polygons.forEach(function(polygon){if(!polygon._){var component=[],neighbors=[polygon];polygon._=1;components.push(component);while(polygon=neighbors.pop()){component.push(polygon);polygon.forEach(function(ring){ring.forEach(function(arc){polygonsByArc[arc<0?~arc:arc].forEach(function(polygon){if(!polygon._){polygon._=1;neighbors.push(polygon)}})})})}}});polygons.forEach(function(polygon){delete polygon._});return{type:"MultiPolygon",arcs:components.map(function(polygons){var arcs=[];polygons.forEach(function(polygon){polygon.forEach(function(ring){ring.forEach(function(arc){if(polygonsByArc[arc<0?~arc:arc].length<2){arcs.push(arc)}})})});arcs=stitchArcs(topology,arcs);if((n=arcs.length)>1){var sgn=exterior(polygons[0][0]);for(var i=0,t;i<n;++i){if(sgn===exterior(arcs[i])){t=arcs[0],arcs[0]=arcs[i],arcs[i]=t;break}}}return arcs})}}function featureOrCollection(topology,o){return o.type==="GeometryCollection"?{type:"FeatureCollection",features:o.geometries.map(function(o){return feature(topology,o)})}:feature(topology,o)}function feature(topology,o){var f={type:"Feature",id:o.id,properties:o.properties||{},geometry:object(topology,o)};if(o.id==null)delete f.id;return f}function object(topology,o){var absolute=transformAbsolute(topology.transform),arcs=topology.arcs;function arc(i,points){if(points.length)points.pop();for(var a=arcs[i<0?~i:i],k=0,n=a.length,p;k<n;++k){points.push(p=a[k].slice());absolute(p,k)}if(i<0)reverse(points,n)}function point(p){p=p.slice();absolute(p,0);return p}function line(arcs){var points=[];for(var i=0,n=arcs.length;i<n;++i)arc(arcs[i],points);if(points.length<2)points.push(points[0].slice());return points}function ring(arcs){var points=line(arcs);while(points.length<4)points.push(points[0].slice());return points}function polygon(arcs){return arcs.map(ring)}function geometry(o){var t=o.type;return t==="GeometryCollection"?{type:t,geometries:o.geometries.map(geometry)}:t in geometryType?{type:t,coordinates:geometryType[t](o)}:null}var geometryType={Point:function(o){return point(o.coordinates)},MultiPoint:function(o){return o.coordinates.map(point)},LineString:function(o){return line(o.arcs)},MultiLineString:function(o){return o.arcs.map(line)},Polygon:function(o){return polygon(o.arcs)},MultiPolygon:function(o){return o.arcs.map(polygon)}};return geometry(o)}function reverse(array,n){var t,j=array.length,i=j-n;while(i<--j)t=array[i],array[i++]=array[j],array[j]=t}function bisect(a,x){var lo=0,hi=a.length;while(lo<hi){var mid=lo+hi>>>1;if(a[mid]<x)lo=mid+1;else hi=mid}return lo}function neighbors(objects){var indexesByArc={},neighbors=objects.map(function(){return[]});function line(arcs,i){arcs.forEach(function(a){if(a<0)a=~a;var o=indexesByArc[a];if(o)o.push(i);else indexesByArc[a]=[i]})}function polygon(arcs,i){arcs.forEach(function(arc){line(arc,i)})}function geometry(o,i){if(o.type==="GeometryCollection")o.geometries.forEach(function(o){geometry(o,i)});else if(o.type in geometryType)geometryType[o.type](o.arcs,i)}var geometryType={LineString:line,MultiLineString:polygon,Polygon:polygon,MultiPolygon:function(arcs,i){arcs.forEach(function(arc){polygon(arc,i)})}};objects.forEach(geometry);for(var i in indexesByArc){for(var indexes=indexesByArc[i],m=indexes.length,j=0;j<m;++j){for(var k=j+1;k<m;++k){var ij=indexes[j],ik=indexes[k],n;if((n=neighbors[ij])[i=bisect(n,ik)]!==ik)n.splice(i,0,ik);if((n=neighbors[ik])[i=bisect(n,ij)]!==ij)n.splice(i,0,ij)}}}return neighbors}function presimplify(topology,triangleArea){var absolute=transformAbsolute(topology.transform),relative=transformRelative(topology.transform),heap=minAreaHeap();if(!triangleArea)triangleArea=cartesianTriangleArea;topology.arcs.forEach(function(arc){var triangles=[],maxArea=0,triangle;for(var i=0,n=arc.length,p;i<n;++i){p=arc[i];arc[i]=[p[0],p[1],Infinity,p[0],p[1]];absolute(arc[i],i)}for(var i=1,n=arc.length-1;i<n;++i){triangle=arc.slice(i-1,i+2);triangle[1][2]=triangleArea(triangle);triangles.push(triangle);heap.push(triangle)}for(var i=0,n=triangles.length;i<n;++i){triangle=triangles[i];triangle.previous=triangles[i-1];triangle.next=triangles[i+1]}while(triangle=heap.pop()){var previous=triangle.previous,next=triangle.next;if(triangle[1][2]<maxArea)triangle[1][2]=maxArea;else maxArea=triangle[1][2];if(previous){previous.next=next;previous[2]=triangle[2];update(previous)}if(next){next.previous=previous;next[0]=triangle[0];update(next)}}for(var i=0,n=arc.length,p;i<n;++i){p=arc[i];arc[i]=[p[3],p[4],p[2]]}});function update(triangle){heap.remove(triangle);triangle[1][2]=triangleArea(triangle);heap.push(triangle)}return topology}function cartesianRingArea(ring){var i=-1,n=ring.length,a,b=ring[n-1],area=0;while(++i<n){a=b;b=ring[i];area+=a[0]*b[1]-a[1]*b[0]}return area*.5}function cartesianTriangleArea(triangle){var a=triangle[0],b=triangle[1],c=triangle[2];return Math.abs((a[0]-c[0])*(b[1]-a[1])-(a[0]-b[0])*(c[1]-a[1]))}function compareArea(a,b){return a[1][2]-b[1][2]}function minAreaHeap(){var heap={},array=[],size=0;heap.push=function(object){up(array[object._=size]=object,size++);return size};heap.pop=function(){if(size<=0)return;var removed=array[0],object;if(--size>0)object=array[size],down(array[object._=0]=object,0);return removed};heap.remove=function(removed){var i=removed._,object;if(array[i]!==removed)return;if(i!==--size)object=array[size],(compareArea(object,removed)<0?up:down)(array[object._=i]=object,i);return i};function up(object,i){while(i>0){var j=(i+1>>1)-1,parent=array[j];if(compareArea(object,parent)>=0)break;array[parent._=i]=parent;array[object._=i=j]=object}}function down(object,i){while(true){var r=i+1<<1,l=r-1,j=i,child=array[j];if(l<size&&compareArea(array[l],child)<0)child=array[j=l];if(r<size&&compareArea(array[r],child)<0)child=array[j=r];if(j===i)break;array[child._=i]=child;array[object._=i=j]=object}}return heap}function transformAbsolute(transform){if(!transform)return noop;var x0,y0,kx=transform.scale[0],ky=transform.scale[1],dx=transform.translate[0],dy=transform.translate[1];return function(point,i){if(!i)x0=y0=0;point[0]=(x0+=point[0])*kx+dx;point[1]=(y0+=point[1])*ky+dy}}function transformRelative(transform){if(!transform)return noop;var x0,y0,kx=transform.scale[0],ky=transform.scale[1],dx=transform.translate[0],dy=transform.translate[1];return function(point,i){if(!i)x0=y0=0;var x1=(point[0]-dx)/kx|0,y1=(point[1]-dy)/ky|0;point[0]=x1-x0;point[1]=y1-y0;x0=x1;y0=y1}}function noop(){}if(typeof define==="function"&&define.amd)define(topojson);else if(typeof module==="object"&&module.exports)module.exports=topojson;else this.topojson=topojson}();
  2. martgnz revised this gist Apr 30, 2016. 2 changed files with 25 additions and 23 deletions.
    46 changes: 24 additions & 22 deletions index.html
    Original file line number Diff line number Diff line change
    @@ -23,30 +23,32 @@
    data: [
    {
    features: topojson.feature(d, d.objects["antarctica"]),
    paintfeature: function(parameters, d) {
    parameters.context.lineWidth = 0.5 / parameters.scale
    parameters.context.strokeStyle = "rgb(170,170,170)"
    parameters.context.stroke()
    static: {
    paintfeature: function(parameters, d) {
    parameters.context.lineWidth = 0.5 / parameters.scale
    parameters.context.strokeStyle = "rgb(170,170,170)"
    parameters.context.stroke()

    parameters.context.fillStyle = "rgba(51, 103, 153, 0.1)"
    parameters.context.fill()
    },
    postpaint: function(parameters, d) {
    antarctica.filter(function(d) {
    return d.countries === "Russia"
    })
    .forEach(function(d) {
    var projectedPoint = parameters.map.settings().projection([+d.lng, +d.lat])

    parameters.context.beginPath()
    parameters.context.textAlign = "center"
    parameters.context.font = "bold 12px sans-serif"
    parameters.context.fillStyle = "rgb(70,70,70)"

    parameters.context.arc(projectedPoint[0], projectedPoint[1] / parameters.scale, 2 / parameters.scale, 0, 2 * Math.PI, true)
    parameters.context.fillText(d.name, projectedPoint[0], projectedPoint[1] - 5)
    parameters.context.fillStyle = "rgba(51, 103, 153, 0.1)"
    parameters.context.fill()
    })
    },
    postpaint: function(parameters, d) {
    antarctica.filter(function(d) {
    return d.countries === "Russia"
    })
    .forEach(function(d) {
    var projectedPoint = parameters.map.settings().projection([+d.lng, +d.lat])

    parameters.context.beginPath()
    parameters.context.textAlign = "center"
    parameters.context.font = "bold 12px sans-serif"
    parameters.context.fillStyle = "rgb(70,70,70)"

    parameters.context.arc(projectedPoint[0], projectedPoint[1] / parameters.scale, 2 / parameters.scale, 0, 2 * Math.PI, true)
    parameters.context.fillText(d.name, projectedPoint[0], projectedPoint[1] - 5)
    parameters.context.fill()
    })
    }
    }
    }
    ]
    2 changes: 1 addition & 1 deletion spam.min.js
    Original file line number Diff line number Diff line change
    @@ -1 +1 @@
    var StaticCanvasMap;var ZoomableCanvasMap;!function(){"use strict";function inside(pt,polygon){var polys=polygon.geometry.coordinates;if(polygon.geometry.type==="Polygon")polys=[polys];var insidePoly=false;var i=0;while(i<polys.length&&!insidePoly){if(inRing(pt,polys[i][0])){var inHole=false;var k=1;while(k<polys[i].length&&!inHole){if(inRing(pt,polys[i][k])){inHole=true}k++}if(!inHole)insidePoly=true}i++}return insidePoly}function inRing(pt,ring){var isInside=false;for(var i=0,j=ring.length-1;i<ring.length;j=i++){var xi=ring[i][0],yi=ring[i][1];var xj=ring[j][0],yj=ring[j][1];var intersect=yi>pt[1]!==yj>pt[1]&&pt[0]<(xj-xi)*(pt[1]-yi)/(yj-yi)+xi;if(intersect)isInside=!isInside}return isInside}function maxBounds(one,two){var bounds=two;if(one[0][0]<two[0][0])bounds[0][0]=one[0][0];if(one[0][1]<two[0][1])bounds[0][1]=one[0][1];if(one[1][0]>two[1][0])bounds[1][0]=one[1][0];if(one[1][1]>two[1][1])bounds[1][1]=one[1][1];return bounds}function createRTree(element,dataPath){element.lookupTree=rbush(4);var elements=[];for(var j in element.features.features){var bounds=dataPath.bounds(element.features.features[j]);elements.push([bounds[0][0].toFixed(0),bounds[0][1].toFixed(0),Math.ceil(bounds[1][0]),Math.ceil(bounds[1][1]),element.features.features[j]])}element.lookupTree.load(elements)}function paintFeature(element,feature,parameters){parameters.context.beginPath();parameters.path(feature);element.paintfeature(parameters,feature)}function paintBackgroundElement(element,parameters){if(element.prepaint)element.prepaint(parameters);if(element.paintfeature){var lookup=element.lookupTree.search([parameters.translate[0],parameters.translate[1],parameters.width/parameters.scale-parameters.translate[0],parameters.height/parameters.scale-parameters.translate[1]]);for(var j in lookup){paintFeature(element,lookup[j][4],parameters)}}if(element.postpaint)element.postpaint(parameters)}function PartialPainter(data,parameters){var index=0,j=0,element=null,currentLookup=[];this.hasNext=function(){return index<=data.length&&j<currentLookup.length};this.renderNext=function(){if(index>=data.length&&j>=currentLookup.length)return;var start=performance.now();if(!element||j>=currentLookup.length){element=data[index];if(element.prepaint)element.prepaint(parameters);currentLookup=element.lookupTree.search([-parameters.translate[0],-parameters.translate[1],parameters.width/parameters.scale-parameters.translate[0],parameters.height/parameters.scale-parameters.translate[1]]);j=0;++index}if(element.paintfeature){for(;j!=currentLookup.length;++j){var feature=currentLookup[j][4];paintFeature(element,feature,parameters);if(performance.now()-start>10)break}}else{j=currentLookup.length}if(j==currentLookup.length&&element.postpaint){element.postpaint(parameters)}};this.finish=function(){if(index>=data.length&&j>=currentLookup.length)return;if(j<currentLookup.length)index--;for(;index!=data.length;++index){if(j>=currentLookup.length){element=data[index];if(element.prepaint)element.prepaint(parameters);currentLookup=element.lookupTree.search([-parameters.translate[0],-parameters.translate[1],parameters.width/parameters.scale-parameters.translate[0],parameters.height/parameters.scale-parameters.translate[1]]);j=0}for(;j!=currentLookup.length;++j){var feature=currentLookup[j][4];paintFeature(element,feature,parameters)}if(element.postpaint)element.postpaint(parameters)}index=0}}function translatePoint(point,scale,translate){return[point[0]/scale-translate[0],point[1]/scale-translate[1]]}function extend(extension,obj){var newObj={};for(var elem in obj){newObj[elem]=obj[elem]}for(var elem in extension){if(!newObj.hasOwnProperty(elem))newObj[elem]=extension[elem]}return newObj}function CanvasMap(parameters){var settings=extend({width:d3.select(parameters.element).node().getBoundingClientRect().width,ratio:1,area:0,scale:1,translate:[0,0],background:null,backgroundScale:1,backgroundTranslate:[0,0],map:this},parameters),simplify=d3.geo.transform({point:function(x,y,z){if(!z||z>=settings.area){this.stream.point(x,y)}}}),canvas=null,context=null;if(!parameters.projection){var b=[[Infinity,Infinity],[-Infinity,-Infinity]];for(var i in settings.data){b=maxBounds(b,d3.geo.bounds(settings.data[i].features))}settings.projection=d3.geo.mercator().scale(1).center([(b[1][0]+b[0][0])/2,(b[1][1]+b[0][1])/2])}var dataPath=d3.geo.path().projection({stream:function(s){return simplify.stream(settings.projection.stream(s))}});var b=[[Infinity,Infinity],[-Infinity,-Infinity]];for(var i in settings.data){b=maxBounds(b,dataPath.bounds(settings.data[i].features))}var dx=b[1][0]-b[0][0],dy=b[1][1]-b[0][1];if(!parameters.projection){settings.height=settings.height||Math.ceil(dy*settings.width/dx);settings.projection.scale(.9*(settings.width/dx)).translate([settings.width/2,settings.height/2])}else if(!settings.height){settings.height=Math.ceil(dy*1/.9)}d3.select(settings.parameters).attr("height",settings.height);function init(){canvas=d3.select(settings.element).append("canvas");context=canvas.node().getContext("2d");var devicePixelRatio=window.devicePixelRatio||1,backingStoreRatio=context.webkitBackingStorePixelRatio||context.mozBackingStorePixelRatio||context.msBackingStorePixelRatio||context.oBackingStorePixelRatio||context.backingStorePixelRatio||1;settings.ratio=devicePixelRatio/backingStoreRatio;settings.area=1/settings.projection.scale()/settings.ratio/20;canvas.attr("width",settings.width*settings.ratio);canvas.attr("height",settings.height*settings.ratio);canvas.style("width",settings.width+"px");canvas.style("height",settings.height+"px");context.lineJoin="round";context.lineCap="round";dataPath.context(context);context.clearRect(0,0,settings.width*settings.ratio,settings.height*settings.ratio);context.save();context.scale(settings.ratio,settings.ratio);for(var i in settings.data){createRTree(settings.data[i],dataPath)}settings.background=new Image;settings.backgroundScale=settings.scale;settings.backgroundTranslate=settings.translate;var parameters={path:dataPath,context:context,scale:settings.scale,translate:settings.translate,width:settings.width,height:settings.height,map:settings.map};var callback=function(){for(var i in settings.data){var element=settings.data[i];if(element.dynamicpaint)element.dynamicpaint(parameters,null)}context.restore();canvas.on("click",click).on("mousemove",hover).on("mouseleave",hoverLeave)};for(var i in settings.data){var element=settings.data[i];paintBackgroundElement(element,parameters)}settings.background.onload=callback;settings.background.src=canvas.node().toDataURL();this.init=function(){}}function paint(){context.save();context.scale(settings.scale*settings.ratio,settings.scale*settings.ratio);context.translate(settings.translate[0],settings.translate[1]);context.clearRect(-settings.translate[0],-settings.translate[1],settings.width*settings.ratio,settings.height*settings.ratio);context.rect(-settings.translate[0],-settings.translate[1],settings.width/settings.scale,settings.height/settings.scale);context.clip();context.drawImage(settings.background,0,0,settings.width*settings.ratio,settings.height*settings.ratio,-settings.backgroundTranslate[0],-settings.backgroundTranslate[1],settings.width/settings.backgroundScale,settings.height/settings.backgroundScale);var parameters={path:dataPath,context:dataPath.context(),scale:settings.scale,translate:settings.translate,width:settings.width,height:settings.height,map:settings.map};settings.area=1/settings.projection.scale()/settings.scale/settings.ratio/20;for(var i in settings.data){var element=settings.data[i];if(element.dynamicpaint)element.dynamicpaint(parameters,element.hoverElement)}context.restore()}function click(){var point=translatePoint(d3.mouse(this),settings.scale,settings.translate);var parameters={path:dataPath,context:context,scale:settings.scale,translate:settings.translate,width:settings.width,height:settings.height,map:settings.map};for(var i in settings.data){var element=settings.data[i];if(!element.click)continue;var lookup=element.lookupTree.search([point[0],point[1],point[0],point[1]]);var isInside=false;for(var j in lookup){var feature=lookup[j][4];if(inside(settings.projection.invert(point),feature)){element.click(parameters,feature);isInside=true}}isInside||element.click(parameters,null)}}function hoverLeave(){for(var i in settings.data){var element=settings.data[i];element.hoverElement=false}paint()}function hover(){var point=translatePoint(d3.mouse(this),settings.scale,settings.translate),repaint=false;for(var i in settings.data){var element=settings.data[i];if(element.hoverElement&&inside(settings.projection.invert(point),element.hoverElement)){continue}element.hoverElement=false;var lookup=element.lookupTree.search([point[0],point[1],point[0],point[1]]);for(var j in lookup){var feature=lookup[j][4];if(inside(settings.projection.invert(point),feature)){element.hoverElement=feature;break}}repaint=true}repaint&&paint()}this.init=init;this.paint=paint;this.settings=function(){return settings}}StaticCanvasMap=function(parameters){var map=new CanvasMap(parameters);this.init=function(){map.init()};this.paint=function(){map.paint()}};var epsilon=.5;function nearEqual(a,b){return Math.abs(a-b)<epsilon}function ImageCache(parameters){var cache=[],settings=parameters;this.addImage=function(parameters){cache.push(parameters)};this.getImage=function(parameters){for(var i in cache){var element=cache[i];if(nearEqual(element.scale,parameters.scale)&&nearEqual(element.translate[0],parameters.translate[0])&&nearEqual(element.translate[1],parameters.translate[1]))return element}return null};this.getFittingImage=function(bbox){var currentImage=cache.length>0?cache[0]:null;for(var i in cache){var image=cache[i];var imageBB=[-image.translate[0],-image.translate[1],settings.width/image.scale-image.translate[0],settings.height/image.scale-image.translate[1]];if(imageBB[0]<=bbox[0]&&imageBB[1]<=bbox[1]&&imageBB[2]>=bbox[2]&&imageBB[3]>=bbox[3]&&(!currentImage||currentImage.scale<image.scale)){currentImage=image}}return currentImage}}ZoomableCanvasMap=function(parameters){var map=new CanvasMap(parameters),simplify=d3.geo.transform({point:function(x,y,z){if(z>=area)this.stream.point(x,y)}}),area=0,canvas=null,context=null,settings=map.settings(),dataPath=d3.geo.path().projection({stream:function(s){return simplify.stream(settings.projection.stream(s))}}),imageCache=new ImageCache({width:settings.width,height:settings.height});settings.map=this;settings.zoomScaleFactor=settings.zoomScaleFactor||.5;this.init=function(){map.init();canvas=d3.select(settings.element).append("canvas");context=canvas.node().getContext("2d");area=1/settings.projection.scale()/settings.ratio/20;canvas.attr("width",settings.width*settings.ratio);canvas.attr("height",settings.height*settings.ratio);canvas.style("width",settings.width+"px");canvas.style("height",settings.height+"px");canvas.style("display","none");context.lineJoin="round";context.lineCap="round";dataPath.context(context);imageCache.addImage({image:settings.background,scale:settings.scale,translate:settings.translate})};this.paint=function(){map.paint()};function scaleZoom(scale,translate){if(nearEqual(scale,settings.scale)&&nearEqual(translate[0],settings.translate[0])&&nearEqual(translate[1],settings.translate[1])){scale=1;translate=[0,0]}if(scale==1&&settings.scale==1&&!translate[0]&&!translate[1]&&!settings.translate[0]&&!settings.translate[1]){return}area=1/settings.projection.scale()/scale/settings.ratio/20;context.save();context.scale(scale*settings.ratio,scale*settings.ratio);context.translate(translate[0],translate[1]);context.clearRect(-translate[0],-translate[1],settings.width*settings.ratio,settings.height*settings.ratio);var parameters={path:dataPath,context:context,scale:scale,translate:translate,width:settings.width,height:settings.height,map:settings.map};var image=imageCache.getImage({scale:scale,translate:translate});if(!image){var background=new Image,partialPainter=new PartialPainter(settings.data,parameters)}var translatedOne=translatePoint([settings.width,settings.height],scale,translate),translatedTwo=translatePoint([settings.width,settings.height],settings.scale,settings.translate);var bbox=[Math.min(-translate[0],-settings.translate[0]),Math.min(-translate[1],-settings.translate[1]),Math.max(translatedOne[0],translatedTwo[0]),Math.max(translatedOne[1],translatedTwo[1])];var zoomImage=imageCache.getFittingImage(bbox);if(zoomImage){settings.background=zoomImage.image;settings.backgroundScale=zoomImage.scale;settings.backgroundTranslate=zoomImage.translate}d3.transition().duration(300).ease("linear").tween("zoom",function(){var i=d3.interpolateNumber(settings.scale,scale),oldTranslate=settings.translate,oldScale=settings.scale;return function(t){settings.scale=i(t);var newTranslate=[oldTranslate[0]+(translate[0]-oldTranslate[0])/(scale-oldScale)*(i(t)-oldScale)*scale/i(t),oldTranslate[1]+(translate[1]-oldTranslate[1])/(scale-oldScale)*(i(t)-oldScale)*scale/i(t)];settings.translate=newTranslate;map.paint();!image&&partialPainter.renderNext()}}).each("end",function(){settings.scale=scale;settings.translate=translate;if(image){context.restore();settings.background=image.image;settings.backgroundScale=image.scale;settings.backgroundTranslate=image.translate;map.paint()}else{partialPainter.finish();background.onload=function(){context.restore();imageCache.addImage({image:background,scale:scale,translate:translate});settings.background=background;settings.backgroundScale=scale;settings.backgroundTranslate=translate;map.paint()};background.src=canvas.node().toDataURL()}})}this.zoom=function(d){if(!d){scaleZoom.call(this,1,[0,0]);return}var bounds=dataPath.bounds(d),dx=bounds[1][0]-bounds[0][0],dy=bounds[1][1]-bounds[0][1],bx=(bounds[0][0]+bounds[1][0])/2,by=(bounds[0][1]+bounds[1][1])/2,scale=settings.zoomScaleFactor*Math.min(settings.width/dx,settings.height/dy),translate=[-bx+settings.width/scale/2,-by+settings.height/scale/2];scaleZoom.call(this,scale,translate)}}}();
    var StaticCanvasMap;var ZoomableCanvasMap;!function(){"use strict";function inside(pt,polygon){var polys=polygon.geometry.coordinates;if(polygon.geometry.type==="Polygon")polys=[polys];var insidePoly=false;var i=0;while(i<polys.length&&!insidePoly){if(inRing(pt,polys[i][0])){var inHole=false;var k=1;while(k<polys[i].length&&!inHole){if(inRing(pt,polys[i][k])){inHole=true}k++}if(!inHole)insidePoly=true}i++}return insidePoly}function inRing(pt,ring){var isInside=false;for(var i=0,j=ring.length-1;i<ring.length;j=i++){var xi=ring[i][0],yi=ring[i][1];var xj=ring[j][0],yj=ring[j][1];var intersect=yi>pt[1]!==yj>pt[1]&&pt[0]<(xj-xi)*(pt[1]-yi)/(yj-yi)+xi;if(intersect)isInside=!isInside}return isInside}function maxBounds(one,two){var bounds=two;if(one[0][0]<two[0][0])bounds[0][0]=one[0][0];if(one[0][1]<two[0][1])bounds[0][1]=one[0][1];if(one[1][0]>two[1][0])bounds[1][0]=one[1][0];if(one[1][1]>two[1][1])bounds[1][1]=one[1][1];return bounds}function createRTree(element,dataPath){element.lookupTree=rbush(4);var elements=[];for(var j in element.features.features){var bounds=dataPath.bounds(element.features.features[j]);elements.push([bounds[0][0].toFixed(0),bounds[0][1].toFixed(0),Math.ceil(bounds[1][0]),Math.ceil(bounds[1][1]),element.features.features[j]])}element.lookupTree.load(elements)}function paintFeature(element,feature,parameters){parameters.context.beginPath();parameters.path(feature);element.static.paintfeature(parameters,feature)}function paintBackgroundElement(element,parameters){if(!element.static)return;if(element.static.prepaint)element.static.prepaint(parameters);if(element.static.paintfeature){var lookup=element.lookupTree.search([parameters.translate[0],parameters.translate[1],parameters.width/parameters.scale-parameters.translate[0],parameters.height/parameters.scale-parameters.translate[1]]);for(var j in lookup){paintFeature(element,lookup[j][4],parameters)}}if(element.static.postpaint)element.static.postpaint(parameters)}function PartialPainter(data,parameters){var index=0,j=0,element=null,currentLookup=[];this.hasNext=function(){return index<=data.length&&j<currentLookup.length};this.renderNext=function(){if(index>=data.length&&j>=currentLookup.length)return;var start=performance.now();if(!element||j>=currentLookup.length){while(index<data.length&&!data[index].static){index++}if(index>=data.length)return;element=data[index];if(element.static.prepaint)element.static.prepaint(parameters);currentLookup=element.lookupTree.search([-parameters.translate[0],-parameters.translate[1],parameters.width/parameters.scale-parameters.translate[0],parameters.height/parameters.scale-parameters.translate[1]]);j=0;++index}if(element.static.paintfeature){for(;j!=currentLookup.length;++j){var feature=currentLookup[j][4];paintFeature(element,feature,parameters);if(performance.now()-start>10)break}}else{j=currentLookup.length}if(j==currentLookup.length&&element.static.postpaint){element.static.postpaint(parameters)}};this.finish=function(){if(index>=data.length&&j>=currentLookup.length)return;if(j<currentLookup.length)index--;for(;index!=data.length;++index){if(j>=currentLookup.length){while(!data[index].static&&index<data.length){index++}if(index>=data.length)return;element=data[index];if(element.static.prepaint)element.static.prepaint(parameters);currentLookup=element.lookupTree.search([-parameters.translate[0],-parameters.translate[1],parameters.width/parameters.scale-parameters.translate[0],parameters.height/parameters.scale-parameters.translate[1]]);j=0}if(element.static.paintfeature){for(;j!=currentLookup.length;++j){var feature=currentLookup[j][4];paintFeature(element,feature,parameters)}}if(element.static.postpaint)element.static.postpaint(parameters)}}}function translatePoint(point,scale,translate){return[point[0]/scale-translate[0],point[1]/scale-translate[1]]}function extend(extension,obj){var newObj={};for(var elem in obj){newObj[elem]=obj[elem]}for(var elem in extension){if(!newObj.hasOwnProperty(elem))newObj[elem]=extension[elem]}return newObj}function CanvasMap(parameters){var settings=extend({width:d3.select(parameters.element).node().getBoundingClientRect().width,ratio:1,area:0,scale:1,translate:[0,0],background:null,backgroundScale:1,backgroundTranslate:[0,0],map:this},parameters),simplify=d3.geo.transform({point:function(x,y,z){if(!z||z>=settings.area){this.stream.point(x,y)}}}),canvas=null,context=null;if(!parameters.projection){var b=[[Infinity,Infinity],[-Infinity,-Infinity]];for(var i in settings.data){b=maxBounds(b,d3.geo.bounds(settings.data[i].features))}settings.projection=d3.geo.mercator().scale(1).center([(b[1][0]+b[0][0])/2,(b[1][1]+b[0][1])/2])}var dataPath=d3.geo.path().projection({stream:function(s){return simplify.stream(settings.projection.stream(s))}});var b=[[Infinity,Infinity],[-Infinity,-Infinity]];for(var i in settings.data){b=maxBounds(b,dataPath.bounds(settings.data[i].features))}var dx=b[1][0]-b[0][0],dy=b[1][1]-b[0][1];if(!parameters.projection){settings.height=settings.height||Math.ceil(dy*settings.width/dx);settings.projection.scale(.9*(settings.width/dx)).translate([settings.width/2,settings.height/2])}else if(!settings.height){settings.height=Math.ceil(dy*1/.9)}d3.select(settings.parameters).attr("height",settings.height);function init(){canvas=d3.select(settings.element).append("canvas");context=canvas.node().getContext("2d");var devicePixelRatio=window.devicePixelRatio||1,backingStoreRatio=context.webkitBackingStorePixelRatio||context.mozBackingStorePixelRatio||context.msBackingStorePixelRatio||context.oBackingStorePixelRatio||context.backingStorePixelRatio||1;settings.ratio=devicePixelRatio/backingStoreRatio;settings.area=1/settings.projection.scale()/settings.ratio/20;canvas.attr("width",settings.width*settings.ratio);canvas.attr("height",settings.height*settings.ratio);canvas.style("width",settings.width+"px");canvas.style("height",settings.height+"px");context.lineJoin="round";context.lineCap="round";dataPath.context(context);context.clearRect(0,0,settings.width*settings.ratio,settings.height*settings.ratio);context.save();context.scale(settings.ratio,settings.ratio);for(var i in settings.data){createRTree(settings.data[i],dataPath)}settings.background=new Image;settings.backgroundScale=settings.scale;settings.backgroundTranslate=settings.translate;var parameters={path:dataPath,context:context,scale:settings.scale,translate:settings.translate,width:settings.width,height:settings.height,map:settings.map};var callback=function(){for(var i in settings.data){var element=settings.data[i];if(element.dynamic&&element.dynamic.postpaint)element.dynamic.postpaint(parameters,null)}context.restore();canvas.on("click",click).on("mousemove",hover).on("mouseleave",hoverLeave)};for(var i in settings.data){var element=settings.data[i];if(element.dynamic&&element.dynamic.prepaint)element.dynamic.prepaint(parameters,element.hoverElement)}for(var i in settings.data){var element=settings.data[i];paintBackgroundElement(element,parameters)}settings.background.onload=callback;settings.background.src=canvas.node().toDataURL();this.init=function(){}}function paint(){context.save();context.scale(settings.scale*settings.ratio,settings.scale*settings.ratio);context.translate(settings.translate[0],settings.translate[1]);context.clearRect(-settings.translate[0],-settings.translate[1],settings.width*settings.ratio,settings.height*settings.ratio);context.rect(-settings.translate[0],-settings.translate[1],settings.width/settings.scale,settings.height/settings.scale);context.clip();var parameters={path:dataPath,context:dataPath.context(),scale:settings.scale,translate:settings.translate,width:settings.width,height:settings.height,map:settings.map};settings.area=1/settings.projection.scale()/settings.scale/settings.ratio/20;for(var i in settings.data){var element=settings.data[i];if(element.dynamic&&element.dynamic.prepaint)element.dynamic.prepaint(parameters,element.hoverElement)}context.drawImage(settings.background,0,0,settings.width*settings.ratio,settings.height*settings.ratio,-settings.backgroundTranslate[0],-settings.backgroundTranslate[1],settings.width/settings.backgroundScale,settings.height/settings.backgroundScale);for(var i in settings.data){var element=settings.data[i];if(element.dynamic&&element.dynamic.postpaint)element.dynamic.postpaint(parameters,element.hoverElement)}context.restore()}function click(){var point=translatePoint(d3.mouse(this),settings.scale,settings.translate);var parameters={path:dataPath,context:context,scale:settings.scale,translate:settings.translate,width:settings.width,height:settings.height,map:settings.map};for(var i in settings.data){var element=settings.data[i];if(!element.click)continue;var lookup=element.lookupTree.search([point[0],point[1],point[0],point[1]]);var isInside=false;for(var j in lookup){var feature=lookup[j][4];if(inside(settings.projection.invert(point),feature)){element.click(parameters,feature);isInside=true}}isInside||element.click(parameters,null)}}function hoverLeave(){for(var i in settings.data){var element=settings.data[i];element.hoverElement=false}paint()}function hover(){var point=translatePoint(d3.mouse(this),settings.scale,settings.translate),repaint=false;for(var i in settings.data){var element=settings.data[i];if(element.hoverElement&&inside(settings.projection.invert(point),element.hoverElement)){continue}element.hoverElement=false;var lookup=element.lookupTree.search([point[0],point[1],point[0],point[1]]);for(var j in lookup){var feature=lookup[j][4];if(inside(settings.projection.invert(point),feature)){element.hoverElement=feature;break}}repaint=true}repaint&&paint()}this.init=init;this.paint=paint;this.settings=function(){return settings}}StaticCanvasMap=function(parameters){var map=new CanvasMap(parameters);this.init=function(){map.init()};this.paint=function(){map.paint()}};var epsilon=.5;function nearEqual(a,b){return Math.abs(a-b)<epsilon}function ImageCache(parameters){var cache=[],settings=parameters;this.addImage=function(parameters){cache.push(parameters)};this.getImage=function(parameters){for(var i in cache){var element=cache[i];if(nearEqual(element.scale,parameters.scale)&&nearEqual(element.translate[0],parameters.translate[0])&&nearEqual(element.translate[1],parameters.translate[1]))return element}return null};this.getFittingImage=function(bbox){var currentImage=cache.length>0?cache[0]:null;for(var i in cache){var image=cache[i];var imageBB=[-image.translate[0],-image.translate[1],settings.width/image.scale-image.translate[0],settings.height/image.scale-image.translate[1]];if(imageBB[0]<=bbox[0]&&imageBB[1]<=bbox[1]&&imageBB[2]>=bbox[2]&&imageBB[3]>=bbox[3]&&(!currentImage||currentImage.scale<image.scale)){currentImage=image}}return currentImage}}ZoomableCanvasMap=function(parameters){var map=new CanvasMap(parameters),simplify=d3.geo.transform({point:function(x,y,z){if(z>=area)this.stream.point(x,y)}}),area=0,canvas=null,context=null,settings=map.settings(),dataPath=d3.geo.path().projection({stream:function(s){return simplify.stream(settings.projection.stream(s))}}),imageCache=new ImageCache({width:settings.width,height:settings.height});settings.map=this;settings.zoomScale=settings.zoomScale||.5;this.init=function(){map.init();canvas=d3.select(settings.element).append("canvas");context=canvas.node().getContext("2d");area=1/settings.projection.scale()/settings.ratio/20;canvas.attr("width",settings.width*settings.ratio);canvas.attr("height",settings.height*settings.ratio);canvas.style("width",settings.width+"px");canvas.style("height",settings.height+"px");canvas.style("display","none");context.lineJoin="round";context.lineCap="round";dataPath.context(context);imageCache.addImage({image:settings.background,scale:settings.scale,translate:settings.translate})};this.paint=function(){map.paint()};function scaleZoom(scale,translate){if(nearEqual(scale,settings.scale)&&nearEqual(translate[0],settings.translate[0])&&nearEqual(translate[1],settings.translate[1])){scale=1;translate=[0,0]}if(scale==1&&settings.scale==1&&!translate[0]&&!translate[1]&&!settings.translate[0]&&!settings.translate[1]){return}area=1/settings.projection.scale()/scale/settings.ratio/20;context.save();context.scale(scale*settings.ratio,scale*settings.ratio);context.translate(translate[0],translate[1]);context.clearRect(-translate[0],-translate[1],settings.width*settings.ratio,settings.height*settings.ratio);var parameters={path:dataPath,context:context,scale:scale,translate:translate,width:settings.width,height:settings.height,map:settings.map};var image=imageCache.getImage({scale:scale,translate:translate});if(!image){var background=new Image,partialPainter=new PartialPainter(settings.data,parameters)}var translatedOne=translatePoint([settings.width,settings.height],scale,translate),translatedTwo=translatePoint([settings.width,settings.height],settings.scale,settings.translate);var bbox=[Math.min(-translate[0],-settings.translate[0]),Math.min(-translate[1],-settings.translate[1]),Math.max(translatedOne[0],translatedTwo[0]),Math.max(translatedOne[1],translatedTwo[1])];var zoomImage=imageCache.getFittingImage(bbox);if(zoomImage){settings.background=zoomImage.image;settings.backgroundScale=zoomImage.scale;settings.backgroundTranslate=zoomImage.translate}d3.transition().duration(300).ease("linear").tween("zoom",function(){var i=d3.interpolateNumber(settings.scale,scale),oldTranslate=settings.translate,oldScale=settings.scale;return function(t){settings.scale=i(t);var newTranslate=[oldTranslate[0]+(translate[0]-oldTranslate[0])/(scale-oldScale)*(i(t)-oldScale)*scale/i(t),oldTranslate[1]+(translate[1]-oldTranslate[1])/(scale-oldScale)*(i(t)-oldScale)*scale/i(t)];settings.translate=newTranslate;map.paint();!image&&partialPainter.renderNext()}}).each("end",function(){settings.scale=scale;settings.translate=translate;if(image){context.restore();settings.background=image.image;settings.backgroundScale=image.scale;settings.backgroundTranslate=image.translate;map.paint()}else{partialPainter.finish();background.onload=function(){context.restore();imageCache.addImage({image:background,scale:scale,translate:translate});settings.background=background;settings.backgroundScale=scale;settings.backgroundTranslate=translate;map.paint()};background.src=canvas.node().toDataURL()}})}this.zoom=function(d){if(!d){scaleZoom.call(this,1,[0,0]);return}var bounds=dataPath.bounds(d),dx=bounds[1][0]-bounds[0][0],dy=bounds[1][1]-bounds[0][1],bx=(bounds[0][0]+bounds[1][0])/2,by=(bounds[0][1]+bounds[1][1])/2,scale=settings.zoomScale*Math.min(settings.width/dx,settings.height/dy),translate=[-bx+settings.width/scale/2,-by+settings.height/scale/2];scaleZoom.call(this,scale,translate)}}}();
  3. martgnz revised this gist Apr 9, 2016. 1 changed file with 1 addition and 3 deletions.
    4 changes: 1 addition & 3 deletions index.html
    Original file line number Diff line number Diff line change
    @@ -54,6 +54,4 @@
    map.init()
    })
    })
    </script>
    </body>
    </html>
    </script>
  4. martgnz revised this gist Apr 9, 2016. 3 changed files with 2 additions and 654 deletions.
    2 changes: 1 addition & 1 deletion index.html
    Original file line number Diff line number Diff line change
    @@ -4,7 +4,7 @@
    <script src="d3.min.js"></script>
    <script src="topojson.min.js"></script>
    <script src="rbush.min.js"></script>
    <script src="spam.js"></script>
    <script src="spam.min.js"></script>

    <script type='text/javascript'>
    d3.tsv("antarctic-bases.tsv", function(error, antarctica) {
    653 changes: 0 additions & 653 deletions spam.js
    Original file line number Diff line number Diff line change
    @@ -1,653 +0,0 @@
    var StaticCanvasMap;
    var ZoomableCanvasMap;

    ! function() {
    "use strict";

    // TODO use turf inside as a dependency?
    // Copied from turf.inside
    function inside(pt, polygon) {
    var polys = polygon.geometry.coordinates
    // normalize to multipolygon
    if (polygon.geometry.type === 'Polygon')
    polys = [polys]

    var insidePoly = false
    var i = 0
    while (i < polys.length && !insidePoly) {
    // check if it is in the outer ring first
    if (inRing(pt, polys[i][0])) {
    var inHole = false
    var k = 1
    // check for the point in any of the holes
    while (k < polys[i].length && !inHole) {
    if (inRing(pt, polys[i][k])) {
    inHole = true
    }
    k++
    }
    if(!inHole)
    insidePoly = true
    }
    i++
    }
    return insidePoly
    }

    // pt is [x,y] and ring is [[x,y], [x,y],..]
    function inRing (pt, ring) {
    var isInside = false
    for (var i = 0, j = ring.length - 1; i < ring.length; j = i++) {
    var xi = ring[i][0], yi = ring[i][1]
    var xj = ring[j][0], yj = ring[j][1]
    var intersect = ((yi > pt[1]) !== (yj > pt[1])) &&
    (pt[0] < (xj - xi) * (pt[1] - yi) / (yj - yi) + xi)
    if (intersect) isInside = !isInside
    }
    return isInside
    }

    function maxBounds(one, two) {
    var bounds = two
    if (one[0][0] < two[0][0])
    bounds[0][0] = one[0][0]
    if (one[0][1] < two[0][1])
    bounds[0][1] = one[0][1]
    if (one[1][0] > two[1][0])
    bounds[1][0] = one[1][0]
    if (one[1][1] > two[1][1])
    bounds[1][1] = one[1][1]
    return bounds
    }

    function createRTree(element, dataPath) {
    element.lookupTree = rbush(4)
    var elements = []

    for (var j in element.features.features) {
    var bounds = dataPath.bounds(element.features.features[j])
    elements.push([
    bounds[0][0].toFixed(0),
    bounds[0][1].toFixed(0),
    Math.ceil(bounds[1][0]),
    Math.ceil(bounds[1][1]),
    element.features.features[j]
    ])
    }
    element.lookupTree.load(elements)
    }

    function paintFeature(element, feature, parameters) {
    parameters.context.beginPath()
    parameters.path(feature)
    element.paintfeature(parameters, feature)
    }

    function paintBackgroundElement(element, parameters) {
    if (element.prepaint)
    element.prepaint(parameters)
    if (element.paintfeature) {
    var lookup = element.lookupTree.search([
    parameters.translate[0],
    parameters.translate[1],
    parameters.width / parameters.scale - parameters.translate[0],
    parameters.height / parameters.scale - parameters.translate[1]
    ])
    for (var j in lookup) {
    paintFeature(element, lookup[j][4], parameters)
    }
    }
    if (element.postpaint)
    element.postpaint(parameters)
    }

    function PartialPainter(data, parameters) {
    var index = 0,
    j = 0,
    element = null,
    currentLookup = []

    this.hasNext = function() {
    return index <= data.length && j < currentLookup.length
    }
    this.renderNext = function() {
    if (index >= data.length && j >= currentLookup.length)
    return
    var start = performance.now()
    if (!element || j >= currentLookup.length) {
    element = data[index]

    if (element.prepaint)
    element.prepaint(parameters)
    currentLookup = element.lookupTree.search([
    - parameters.translate[0],
    - parameters.translate[1],
    parameters.width / parameters.scale - parameters.translate[0],
    parameters.height / parameters.scale - parameters.translate[1]
    ])
    j = 0
    ++index
    }
    if (element.paintfeature) {
    for (; j != currentLookup.length; ++j) {
    var feature = currentLookup[j][4]
    paintFeature(element, feature, parameters)
    if ((performance.now() - start) > 10)
    break
    }
    } else {
    j = currentLookup.length
    }
    if (j == currentLookup.length && element.postpaint) {
    element.postpaint(parameters)
    }
    }
    this.finish = function() {
    if (index >= data.length && j >= currentLookup.length)
    return
    if (j < currentLookup.length)
    index--
    for (; index != data.length; ++index) {
    if (j >= currentLookup.length) {
    element = data[index]

    if (element.prepaint)
    element.prepaint(parameters)
    currentLookup = element.lookupTree.search([
    - parameters.translate[0],
    - parameters.translate[1],
    parameters.width / parameters.scale - parameters.translate[0],
    parameters.height / parameters.scale - parameters.translate[1]
    ])
    j = 0
    }
    for (; j != currentLookup.length; ++j) {
    var feature = currentLookup[j][4]
    paintFeature(element, feature, parameters)
    }
    if (element.postpaint)
    element.postpaint(parameters)
    }
    index = 0
    }
    }


    function translatePoint(point, scale, translate) {
    return [
    point[0] / scale - translate[0],
    point[1] / scale - translate[1]
    ]
    }

    function extend(extension, obj) {
    var newObj = {}
    // FIXME this is a bit hacky? Can't we just mutate the original obj? (can't bc projection)
    for (var elem in obj) {
    newObj[elem] = obj[elem]
    }
    for (var elem in extension) {
    if (!newObj.hasOwnProperty(elem))
    newObj[elem] = extension[elem]
    }
    return newObj
    }

    function CanvasMap(parameters) {
    var settings = extend({
    width: d3.select(parameters.element).node().getBoundingClientRect().width,
    ratio: 1,
    area: 0,
    scale: 1,
    translate: [0, 0],
    background: null,
    backgroundScale: 1,
    backgroundTranslate: [0, 0],
    map: this
    }, parameters),
    simplify = d3.geo.transform({
    point: function(x, y, z) {
    if (!z || z >= settings.area) {
    this.stream.point(x, y)
    }
    }
    }),
    canvas = null,
    context = null

    if (!parameters.projection) {
    var b = [[Infinity, Infinity],
    [-Infinity, -Infinity]]
    for (var i in settings.data) {
    b = maxBounds(b, d3.geo.bounds(settings.data[i].features))
    }
    settings.projection = d3.geo.mercator()
    .scale(1)
    .center([(b[1][0] + b[0][0]) / 2, (b[1][1] + b[0][1]) / 2])
    }
    var dataPath = d3.geo.path().projection({
    stream: function(s) {
    return simplify.stream(settings.projection.stream(s))
    }
    })
    var b = [[Infinity, Infinity],
    [-Infinity, -Infinity]]
    for (var i in settings.data) {
    b = maxBounds(b, dataPath.bounds(settings.data[i].features))
    }

    var dx = b[1][0] - b[0][0],
    dy = b[1][1] - b[0][1]

    if (!parameters.projection) {
    settings.height = settings.height || Math.ceil(dy * settings.width / dx)
    settings.projection.scale(0.9 * (settings.width / dx))
    .translate([settings.width / 2, settings.height / 2])
    } else if (!settings.height) {
    settings.height = Math.ceil(dy * 1 / 0.9)
    }
    d3.select(settings.parameters).attr("height", settings.height)

    function init() {
    canvas = d3.select(settings.element)
    .append("canvas")
    context = canvas.node().getContext("2d")

    var devicePixelRatio = window.devicePixelRatio || 1,
    backingStoreRatio = context.webkitBackingStorePixelRatio ||
    context.mozBackingStorePixelRatio ||
    context.msBackingStorePixelRatio ||
    context.oBackingStorePixelRatio ||
    context.backingStorePixelRatio || 1
    settings.ratio = devicePixelRatio / backingStoreRatio
    settings.area = 1 / settings.projection.scale() / settings.ratio / 20

    canvas.attr("width", settings.width * settings.ratio)
    canvas.attr("height", settings.height * settings.ratio)
    canvas.style("width", settings.width + "px")
    canvas.style("height", settings.height + "px")
    context.lineJoin = "round"
    context.lineCap = "round"

    dataPath.context(context)
    context.clearRect(0, 0, settings.width * settings.ratio, settings.height * settings.ratio)
    context.save()
    context.scale(settings.ratio, settings.ratio)

    // TODO move rtree part out?
    for (var i in settings.data) {
    createRTree(settings.data[i], dataPath)
    }

    settings.background = new Image()
    settings.backgroundScale = settings.scale
    settings.backgroundTranslate = settings.translate
    var parameters = {
    path: dataPath,
    context: context,
    scale: settings.scale,
    translate: settings.translate,
    width: settings.width,
    height: settings.height,
    map: settings.map
    }
    var callback = function() {
    for (var i in settings.data) {
    var element = settings.data[i]

    if (element.dynamicpaint)
    element.dynamicpaint(parameters, null)
    }

    context.restore()
    canvas.on("click", click)
    .on("mousemove", hover)
    .on("mouseleave", hoverLeave)
    }
    for (var i in settings.data) {
    var element = settings.data[i]
    paintBackgroundElement(element, parameters)
    }
    settings.background.onload = callback
    settings.background.src = canvas.node().toDataURL()

    //Prevent another call to the init method
    this.init = function() {}
    }

    // TODO probably try to use the same data path in the zoom class, but have a different area settable?

    function paint() {
    context.save()
    context.scale(settings.scale * settings.ratio, settings.scale * settings.ratio)
    context.translate(settings.translate[0], settings.translate[1])

    context.clearRect(- settings.translate[0], - settings.translate[1], settings.width * settings.ratio, settings.height * settings.ratio)

    context.rect(- settings.translate[0], - settings.translate[1],
    settings.width / settings.scale,
    settings.height / settings.scale)
    context.clip()

    context.drawImage(settings.background, 0, 0,
    settings.width * settings.ratio, settings.height * settings.ratio,
    - settings.backgroundTranslate[0],
    - settings.backgroundTranslate[1],
    settings.width / settings.backgroundScale, settings.height / settings.backgroundScale)

    // FIXME this needs a way for the callback to use the lookupTree?
    var parameters = {
    path: dataPath,
    context: dataPath.context(),
    scale: settings.scale,
    translate: settings.translate,
    width: settings.width,
    height: settings.height,
    map: settings.map
    }
    settings.area = 1 / settings.projection.scale() / settings.scale / settings.ratio / 20
    for (var i in settings.data) {
    var element = settings.data[i]
    if (element.dynamicpaint)
    element.dynamicpaint(parameters, element.hoverElement)
    }

    context.restore()
    }

    function click() {
    var point = translatePoint(d3.mouse(this), settings.scale, settings.translate)

    var parameters = {
    path: dataPath,
    context: context,
    scale: settings.scale,
    translate: settings.translate,
    width: settings.width,
    height: settings.height,
    map: settings.map
    }
    for (var i in settings.data) {
    var element = settings.data[i]
    if (!element.click)
    continue

    var lookup = element.lookupTree.search([point[0], point[1], point[0], point[1]])
    var isInside = false
    for (var j in lookup) {
    var feature = lookup[j][4]
    if (inside(settings.projection.invert(point), feature)) {
    element.click(parameters, feature)
    isInside = true
    }
    }
    isInside || element.click(parameters, null)
    }
    }

    function hoverLeave() {
    for (var i in settings.data) {
    var element = settings.data[i]
    element.hoverElement = false
    }
    paint()
    }

    function hover() {
    var point = translatePoint(d3.mouse(this), settings.scale, settings.translate),
    repaint = false

    for (var i in settings.data) {
    var element = settings.data[i]
    if (element.hoverElement &&
    inside(settings.projection.invert(point), element.hoverElement)) {
    continue
    }
    element.hoverElement = false
    var lookup = element.lookupTree.search([point[0], point[1], point[0], point[1]])
    for (var j in lookup) {
    var feature = lookup[j][4]
    if (inside(settings.projection.invert(point), feature)) {
    element.hoverElement = feature
    break
    }
    }
    repaint = true
    }
    repaint && paint()
    }

    this.init = init
    this.paint = paint
    this.settings = function() {
    return settings
    }
    }

    StaticCanvasMap = function(parameters) {
    var map = new CanvasMap(parameters)

    this.init = function() {
    map.init()
    }
    this.paint = function() {
    map.paint()
    }
    }

    var epsilon = 0.5
    function nearEqual(a, b) {
    return Math.abs(a - b) < epsilon
    }

    function ImageCache(parameters) {
    var cache = [],
    settings = parameters

    this.addImage = function(parameters) {
    cache.push(parameters)
    }

    this.getImage = function(parameters) {
    for (var i in cache) {
    var element = cache[i]
    if (nearEqual(element.scale, parameters.scale) &&
    nearEqual(element.translate[0], parameters.translate[0]) &&
    nearEqual(element.translate[1], parameters.translate[1]))
    return element
    }
    return null
    }

    this.getFittingImage = function(bbox) {
    // Auto set scale=1, translate[0, 0] image as default return
    var currentImage = cache.length > 0 ? cache[0] : null
    for (var i in cache) {
    var image = cache[i]
    var imageBB = [
    - image.translate[0],
    - image.translate[1],
    settings.width / image.scale - image.translate[0],
    settings.height / image.scale - image.translate[1]
    ]
    if (imageBB[0] <= bbox[0] &&
    imageBB[1] <= bbox[1] &&
    imageBB[2] >= bbox[2] &&
    imageBB[3] >= bbox[3] &&
    (!currentImage || currentImage.scale < image.scale)) {
    currentImage = image
    }
    }
    return currentImage
    }
    }

    ZoomableCanvasMap = function(parameters) {
    var map = new CanvasMap(parameters),
    simplify = d3.geo.transform({
    point: function(x, y, z) {
    if (z >= area) this.stream.point(x, y)
    }
    }),
    area = 0,
    canvas = null,
    context = null,
    settings = map.settings(),
    dataPath = d3.geo.path().projection({
    stream: function(s) {
    return simplify.stream(settings.projection.stream(s))
    }
    }),
    imageCache = new ImageCache({
    width: settings.width,
    height: settings.height
    })

    settings.map = this
    settings.zoomScaleFactor = settings.zoomScaleFactor || 0.5

    this.init = function() {
    map.init()

    canvas = d3.select(settings.element)
    .append("canvas")
    context = canvas.node().getContext("2d")
    area = 1 / settings.projection.scale() / settings.ratio / 20

    canvas.attr("width", settings.width * settings.ratio)
    canvas.attr("height", settings.height * settings.ratio)
    canvas.style("width", settings.width + "px")
    canvas.style("height", settings.height + "px")
    canvas.style("display", "none")
    context.lineJoin = "round"
    context.lineCap = "round"

    dataPath.context(context)

    imageCache.addImage({
    image: settings.background,
    scale: settings.scale,
    translate: settings.translate
    })
    }
    this.paint = function() {
    map.paint()
    }
    function scaleZoom(scale, translate) {
    if (nearEqual(scale, settings.scale) &&
    nearEqual(translate[0], settings.translate[0]) &&
    nearEqual(translate[1], settings.translate[1])) {
    scale = 1
    translate = [0, 0]
    }
    if (scale == 1 && settings.scale == 1 &&
    !translate[0] && !translate[1] &&
    !settings.translate[0] && !settings.translate[1]) {
    return
    }
    area = 1 / settings.projection.scale() / scale / settings.ratio / 20

    context.save()
    context.scale(scale * settings.ratio, scale * settings.ratio)
    context.translate(translate[0], translate[1])
    context.clearRect(- translate[0], - translate[1], settings.width * settings.ratio, settings.height * settings.ratio)
    var parameters = {
    path: dataPath,
    context: context,
    scale: scale,
    translate: translate,
    width: settings.width,
    height: settings.height,
    map: settings.map
    }

    var image = imageCache.getImage({
    scale: scale,
    translate: translate
    })
    if (!image) {
    var background = new Image(),
    partialPainter = new PartialPainter(settings.data, parameters)
    }

    var translatedOne = translatePoint([settings.width, settings.height], scale, translate),
    translatedTwo = translatePoint([settings.width, settings.height], settings.scale, settings.translate)
    var bbox = [
    Math.min(- translate[0], - settings.translate[0]),
    Math.min(- translate[1], - settings.translate[1]),
    Math.max(translatedOne[0], translatedTwo[0]),
    Math.max(translatedOne[1], translatedTwo[1])
    ]
    var zoomImage = imageCache.getFittingImage(bbox)
    if (zoomImage) {
    settings.background = zoomImage.image
    settings.backgroundScale = zoomImage.scale
    settings.backgroundTranslate = zoomImage.translate
    }
    d3.transition()
    .duration(300)
    .ease("linear")
    .tween("zoom", function() {
    var i = d3.interpolateNumber(settings.scale, scale),
    oldTranslate = settings.translate,
    oldScale = settings.scale
    return function(t) {
    settings.scale = i(t)
    var newTranslate = [
    oldTranslate[0] + (translate[0] - oldTranslate[0]) / (scale - oldScale) * (i(t) - oldScale) * scale / i(t),
    oldTranslate[1] + (translate[1] - oldTranslate[1]) / (scale - oldScale) * (i(t) - oldScale) * scale / i(t),
    ]
    settings.translate = newTranslate
    map.paint()
    !image && partialPainter.renderNext()
    }
    })
    .each("end", function() {
    settings.scale = scale
    settings.translate = translate

    if (image) {
    context.restore()
    settings.background = image.image
    settings.backgroundScale = image.scale
    settings.backgroundTranslate = image.translate
    map.paint()
    } else {
    partialPainter.finish()
    background.onload = function() {
    context.restore()
    imageCache.addImage({
    image: background,
    scale: scale,
    translate: translate
    })
    settings.background = background
    settings.backgroundScale = scale
    settings.backgroundTranslate = translate
    map.paint()
    }
    // TODO there is a function to get the image data from the context, is that faster?
    // TODO use getImageData/putImageData, because it's faster?
    background.src = canvas.node().toDataURL()
    }
    })
    }
    this.zoom = function(d) {
    if (!d) {
    scaleZoom.call(this, 1, [0, 0])
    return
    }
    var bounds = dataPath.bounds(d),
    dx = bounds[1][0] - bounds[0][0],
    dy = bounds[1][1] - bounds[0][1],
    bx = (bounds[0][0] + bounds[1][0]) / 2,
    by = (bounds[0][1] + bounds[1][1]) / 2,
    scale = settings.zoomScaleFactor *
    Math.min(settings.width / dx, settings.height / dy),
    translate = [-bx + settings.width / scale / 2,
    -by + settings.height / scale / 2]

    scaleZoom.call(this, scale, translate)
    }
    }
    }()
    1 change: 1 addition & 0 deletions spam.min.js
    Original file line number Diff line number Diff line change
    @@ -0,0 +1 @@
    var StaticCanvasMap;var ZoomableCanvasMap;!function(){"use strict";function inside(pt,polygon){var polys=polygon.geometry.coordinates;if(polygon.geometry.type==="Polygon")polys=[polys];var insidePoly=false;var i=0;while(i<polys.length&&!insidePoly){if(inRing(pt,polys[i][0])){var inHole=false;var k=1;while(k<polys[i].length&&!inHole){if(inRing(pt,polys[i][k])){inHole=true}k++}if(!inHole)insidePoly=true}i++}return insidePoly}function inRing(pt,ring){var isInside=false;for(var i=0,j=ring.length-1;i<ring.length;j=i++){var xi=ring[i][0],yi=ring[i][1];var xj=ring[j][0],yj=ring[j][1];var intersect=yi>pt[1]!==yj>pt[1]&&pt[0]<(xj-xi)*(pt[1]-yi)/(yj-yi)+xi;if(intersect)isInside=!isInside}return isInside}function maxBounds(one,two){var bounds=two;if(one[0][0]<two[0][0])bounds[0][0]=one[0][0];if(one[0][1]<two[0][1])bounds[0][1]=one[0][1];if(one[1][0]>two[1][0])bounds[1][0]=one[1][0];if(one[1][1]>two[1][1])bounds[1][1]=one[1][1];return bounds}function createRTree(element,dataPath){element.lookupTree=rbush(4);var elements=[];for(var j in element.features.features){var bounds=dataPath.bounds(element.features.features[j]);elements.push([bounds[0][0].toFixed(0),bounds[0][1].toFixed(0),Math.ceil(bounds[1][0]),Math.ceil(bounds[1][1]),element.features.features[j]])}element.lookupTree.load(elements)}function paintFeature(element,feature,parameters){parameters.context.beginPath();parameters.path(feature);element.paintfeature(parameters,feature)}function paintBackgroundElement(element,parameters){if(element.prepaint)element.prepaint(parameters);if(element.paintfeature){var lookup=element.lookupTree.search([parameters.translate[0],parameters.translate[1],parameters.width/parameters.scale-parameters.translate[0],parameters.height/parameters.scale-parameters.translate[1]]);for(var j in lookup){paintFeature(element,lookup[j][4],parameters)}}if(element.postpaint)element.postpaint(parameters)}function PartialPainter(data,parameters){var index=0,j=0,element=null,currentLookup=[];this.hasNext=function(){return index<=data.length&&j<currentLookup.length};this.renderNext=function(){if(index>=data.length&&j>=currentLookup.length)return;var start=performance.now();if(!element||j>=currentLookup.length){element=data[index];if(element.prepaint)element.prepaint(parameters);currentLookup=element.lookupTree.search([-parameters.translate[0],-parameters.translate[1],parameters.width/parameters.scale-parameters.translate[0],parameters.height/parameters.scale-parameters.translate[1]]);j=0;++index}if(element.paintfeature){for(;j!=currentLookup.length;++j){var feature=currentLookup[j][4];paintFeature(element,feature,parameters);if(performance.now()-start>10)break}}else{j=currentLookup.length}if(j==currentLookup.length&&element.postpaint){element.postpaint(parameters)}};this.finish=function(){if(index>=data.length&&j>=currentLookup.length)return;if(j<currentLookup.length)index--;for(;index!=data.length;++index){if(j>=currentLookup.length){element=data[index];if(element.prepaint)element.prepaint(parameters);currentLookup=element.lookupTree.search([-parameters.translate[0],-parameters.translate[1],parameters.width/parameters.scale-parameters.translate[0],parameters.height/parameters.scale-parameters.translate[1]]);j=0}for(;j!=currentLookup.length;++j){var feature=currentLookup[j][4];paintFeature(element,feature,parameters)}if(element.postpaint)element.postpaint(parameters)}index=0}}function translatePoint(point,scale,translate){return[point[0]/scale-translate[0],point[1]/scale-translate[1]]}function extend(extension,obj){var newObj={};for(var elem in obj){newObj[elem]=obj[elem]}for(var elem in extension){if(!newObj.hasOwnProperty(elem))newObj[elem]=extension[elem]}return newObj}function CanvasMap(parameters){var settings=extend({width:d3.select(parameters.element).node().getBoundingClientRect().width,ratio:1,area:0,scale:1,translate:[0,0],background:null,backgroundScale:1,backgroundTranslate:[0,0],map:this},parameters),simplify=d3.geo.transform({point:function(x,y,z){if(!z||z>=settings.area){this.stream.point(x,y)}}}),canvas=null,context=null;if(!parameters.projection){var b=[[Infinity,Infinity],[-Infinity,-Infinity]];for(var i in settings.data){b=maxBounds(b,d3.geo.bounds(settings.data[i].features))}settings.projection=d3.geo.mercator().scale(1).center([(b[1][0]+b[0][0])/2,(b[1][1]+b[0][1])/2])}var dataPath=d3.geo.path().projection({stream:function(s){return simplify.stream(settings.projection.stream(s))}});var b=[[Infinity,Infinity],[-Infinity,-Infinity]];for(var i in settings.data){b=maxBounds(b,dataPath.bounds(settings.data[i].features))}var dx=b[1][0]-b[0][0],dy=b[1][1]-b[0][1];if(!parameters.projection){settings.height=settings.height||Math.ceil(dy*settings.width/dx);settings.projection.scale(.9*(settings.width/dx)).translate([settings.width/2,settings.height/2])}else if(!settings.height){settings.height=Math.ceil(dy*1/.9)}d3.select(settings.parameters).attr("height",settings.height);function init(){canvas=d3.select(settings.element).append("canvas");context=canvas.node().getContext("2d");var devicePixelRatio=window.devicePixelRatio||1,backingStoreRatio=context.webkitBackingStorePixelRatio||context.mozBackingStorePixelRatio||context.msBackingStorePixelRatio||context.oBackingStorePixelRatio||context.backingStorePixelRatio||1;settings.ratio=devicePixelRatio/backingStoreRatio;settings.area=1/settings.projection.scale()/settings.ratio/20;canvas.attr("width",settings.width*settings.ratio);canvas.attr("height",settings.height*settings.ratio);canvas.style("width",settings.width+"px");canvas.style("height",settings.height+"px");context.lineJoin="round";context.lineCap="round";dataPath.context(context);context.clearRect(0,0,settings.width*settings.ratio,settings.height*settings.ratio);context.save();context.scale(settings.ratio,settings.ratio);for(var i in settings.data){createRTree(settings.data[i],dataPath)}settings.background=new Image;settings.backgroundScale=settings.scale;settings.backgroundTranslate=settings.translate;var parameters={path:dataPath,context:context,scale:settings.scale,translate:settings.translate,width:settings.width,height:settings.height,map:settings.map};var callback=function(){for(var i in settings.data){var element=settings.data[i];if(element.dynamicpaint)element.dynamicpaint(parameters,null)}context.restore();canvas.on("click",click).on("mousemove",hover).on("mouseleave",hoverLeave)};for(var i in settings.data){var element=settings.data[i];paintBackgroundElement(element,parameters)}settings.background.onload=callback;settings.background.src=canvas.node().toDataURL();this.init=function(){}}function paint(){context.save();context.scale(settings.scale*settings.ratio,settings.scale*settings.ratio);context.translate(settings.translate[0],settings.translate[1]);context.clearRect(-settings.translate[0],-settings.translate[1],settings.width*settings.ratio,settings.height*settings.ratio);context.rect(-settings.translate[0],-settings.translate[1],settings.width/settings.scale,settings.height/settings.scale);context.clip();context.drawImage(settings.background,0,0,settings.width*settings.ratio,settings.height*settings.ratio,-settings.backgroundTranslate[0],-settings.backgroundTranslate[1],settings.width/settings.backgroundScale,settings.height/settings.backgroundScale);var parameters={path:dataPath,context:dataPath.context(),scale:settings.scale,translate:settings.translate,width:settings.width,height:settings.height,map:settings.map};settings.area=1/settings.projection.scale()/settings.scale/settings.ratio/20;for(var i in settings.data){var element=settings.data[i];if(element.dynamicpaint)element.dynamicpaint(parameters,element.hoverElement)}context.restore()}function click(){var point=translatePoint(d3.mouse(this),settings.scale,settings.translate);var parameters={path:dataPath,context:context,scale:settings.scale,translate:settings.translate,width:settings.width,height:settings.height,map:settings.map};for(var i in settings.data){var element=settings.data[i];if(!element.click)continue;var lookup=element.lookupTree.search([point[0],point[1],point[0],point[1]]);var isInside=false;for(var j in lookup){var feature=lookup[j][4];if(inside(settings.projection.invert(point),feature)){element.click(parameters,feature);isInside=true}}isInside||element.click(parameters,null)}}function hoverLeave(){for(var i in settings.data){var element=settings.data[i];element.hoverElement=false}paint()}function hover(){var point=translatePoint(d3.mouse(this),settings.scale,settings.translate),repaint=false;for(var i in settings.data){var element=settings.data[i];if(element.hoverElement&&inside(settings.projection.invert(point),element.hoverElement)){continue}element.hoverElement=false;var lookup=element.lookupTree.search([point[0],point[1],point[0],point[1]]);for(var j in lookup){var feature=lookup[j][4];if(inside(settings.projection.invert(point),feature)){element.hoverElement=feature;break}}repaint=true}repaint&&paint()}this.init=init;this.paint=paint;this.settings=function(){return settings}}StaticCanvasMap=function(parameters){var map=new CanvasMap(parameters);this.init=function(){map.init()};this.paint=function(){map.paint()}};var epsilon=.5;function nearEqual(a,b){return Math.abs(a-b)<epsilon}function ImageCache(parameters){var cache=[],settings=parameters;this.addImage=function(parameters){cache.push(parameters)};this.getImage=function(parameters){for(var i in cache){var element=cache[i];if(nearEqual(element.scale,parameters.scale)&&nearEqual(element.translate[0],parameters.translate[0])&&nearEqual(element.translate[1],parameters.translate[1]))return element}return null};this.getFittingImage=function(bbox){var currentImage=cache.length>0?cache[0]:null;for(var i in cache){var image=cache[i];var imageBB=[-image.translate[0],-image.translate[1],settings.width/image.scale-image.translate[0],settings.height/image.scale-image.translate[1]];if(imageBB[0]<=bbox[0]&&imageBB[1]<=bbox[1]&&imageBB[2]>=bbox[2]&&imageBB[3]>=bbox[3]&&(!currentImage||currentImage.scale<image.scale)){currentImage=image}}return currentImage}}ZoomableCanvasMap=function(parameters){var map=new CanvasMap(parameters),simplify=d3.geo.transform({point:function(x,y,z){if(z>=area)this.stream.point(x,y)}}),area=0,canvas=null,context=null,settings=map.settings(),dataPath=d3.geo.path().projection({stream:function(s){return simplify.stream(settings.projection.stream(s))}}),imageCache=new ImageCache({width:settings.width,height:settings.height});settings.map=this;settings.zoomScaleFactor=settings.zoomScaleFactor||.5;this.init=function(){map.init();canvas=d3.select(settings.element).append("canvas");context=canvas.node().getContext("2d");area=1/settings.projection.scale()/settings.ratio/20;canvas.attr("width",settings.width*settings.ratio);canvas.attr("height",settings.height*settings.ratio);canvas.style("width",settings.width+"px");canvas.style("height",settings.height+"px");canvas.style("display","none");context.lineJoin="round";context.lineCap="round";dataPath.context(context);imageCache.addImage({image:settings.background,scale:settings.scale,translate:settings.translate})};this.paint=function(){map.paint()};function scaleZoom(scale,translate){if(nearEqual(scale,settings.scale)&&nearEqual(translate[0],settings.translate[0])&&nearEqual(translate[1],settings.translate[1])){scale=1;translate=[0,0]}if(scale==1&&settings.scale==1&&!translate[0]&&!translate[1]&&!settings.translate[0]&&!settings.translate[1]){return}area=1/settings.projection.scale()/scale/settings.ratio/20;context.save();context.scale(scale*settings.ratio,scale*settings.ratio);context.translate(translate[0],translate[1]);context.clearRect(-translate[0],-translate[1],settings.width*settings.ratio,settings.height*settings.ratio);var parameters={path:dataPath,context:context,scale:scale,translate:translate,width:settings.width,height:settings.height,map:settings.map};var image=imageCache.getImage({scale:scale,translate:translate});if(!image){var background=new Image,partialPainter=new PartialPainter(settings.data,parameters)}var translatedOne=translatePoint([settings.width,settings.height],scale,translate),translatedTwo=translatePoint([settings.width,settings.height],settings.scale,settings.translate);var bbox=[Math.min(-translate[0],-settings.translate[0]),Math.min(-translate[1],-settings.translate[1]),Math.max(translatedOne[0],translatedTwo[0]),Math.max(translatedOne[1],translatedTwo[1])];var zoomImage=imageCache.getFittingImage(bbox);if(zoomImage){settings.background=zoomImage.image;settings.backgroundScale=zoomImage.scale;settings.backgroundTranslate=zoomImage.translate}d3.transition().duration(300).ease("linear").tween("zoom",function(){var i=d3.interpolateNumber(settings.scale,scale),oldTranslate=settings.translate,oldScale=settings.scale;return function(t){settings.scale=i(t);var newTranslate=[oldTranslate[0]+(translate[0]-oldTranslate[0])/(scale-oldScale)*(i(t)-oldScale)*scale/i(t),oldTranslate[1]+(translate[1]-oldTranslate[1])/(scale-oldScale)*(i(t)-oldScale)*scale/i(t)];settings.translate=newTranslate;map.paint();!image&&partialPainter.renderNext()}}).each("end",function(){settings.scale=scale;settings.translate=translate;if(image){context.restore();settings.background=image.image;settings.backgroundScale=image.scale;settings.backgroundTranslate=image.translate;map.paint()}else{partialPainter.finish();background.onload=function(){context.restore();imageCache.addImage({image:background,scale:scale,translate:translate});settings.background=background;settings.backgroundScale=scale;settings.backgroundTranslate=translate;map.paint()};background.src=canvas.node().toDataURL()}})}this.zoom=function(d){if(!d){scaleZoom.call(this,1,[0,0]);return}var bounds=dataPath.bounds(d),dx=bounds[1][0]-bounds[0][0],dy=bounds[1][1]-bounds[0][1],bx=(bounds[0][0]+bounds[1][0])/2,by=(bounds[0][1]+bounds[1][1])/2,scale=settings.zoomScaleFactor*Math.min(settings.width/dx,settings.height/dy),translate=[-bx+settings.width/scale/2,-by+settings.height/scale/2];scaleZoom.call(this,scale,translate)}}}();
  5. martgnz revised this gist Apr 8, 2016. 9 changed files with 21 additions and 10730 deletions.
    9,563 changes: 0 additions & 9,563 deletions d3.js
    0 additions, 9,563 deletions not shown because the diff is too large. Please use a local Git client to view these changes.
    8 changes: 8 additions & 0 deletions d3.min.js
    8 additions, 0 deletions not shown because the diff is too large. Please use a local Git client to view these changes.
    15 changes: 9 additions & 6 deletions index.html
    Original file line number Diff line number Diff line change
    @@ -1,9 +1,9 @@
    <!DOCTYPE html>
    <meta charset="utf-8" />
    <body>
    <script src="d3.js"></script>
    <script src="topojson.js"></script>
    <script src="rbush.js"></script>
    <script src="d3.min.js"></script>
    <script src="topojson.min.js"></script>
    <script src="rbush.min.js"></script>
    <script src="spam.js"></script>

    <script type='text/javascript'>
    @@ -27,6 +27,9 @@
    parameters.context.lineWidth = 0.5 / parameters.scale
    parameters.context.strokeStyle = "rgb(170,170,170)"
    parameters.context.stroke()

    parameters.context.fillStyle = "rgba(51, 103, 153, 0.1)"
    parameters.context.fill()
    },
    postpaint: function(parameters, d) {
    antarctica.filter(function(d) {
    @@ -37,11 +40,11 @@

    parameters.context.beginPath()
    parameters.context.textAlign = "center"
    parameters.context.font = "bold " + (12 / parameters.scale) + "px " + "sans-serif"
    parameters.context.font = "bold 12px sans-serif"
    parameters.context.fillStyle = "rgb(70,70,70)"

    parameters.context.arc(projectedPoint[0], projectedPoint[1] + 4 / parameters.scale, 2 / parameters.scale, 0, 2 * Math.PI, true)
    parameters.context.fillText(d.name, projectedPoint[0], projectedPoint[1])
    parameters.context.arc(projectedPoint[0], projectedPoint[1] / parameters.scale, 2 / parameters.scale, 0, 2 * Math.PI, true)
    parameters.context.fillText(d.name, projectedPoint[0], projectedPoint[1] - 5)
    parameters.context.fill()
    })
    }
    621 changes: 0 additions & 621 deletions rbush.js
    Original file line number Diff line number Diff line change
    @@ -1,621 +0,0 @@
    /*
    (c) 2015, Vladimir Agafonkin
    RBush, a JavaScript library for high-performance 2D spatial indexing of points and rectangles.
    https://github.com/mourner/rbush
    */

    (function () {
    'use strict';

    function rbush(maxEntries, format) {

    // jshint newcap: false, validthis: true
    if (!(this instanceof rbush)) return new rbush(maxEntries, format);

    // max entries in a node is 9 by default; min node fill is 40% for best performance
    this._maxEntries = Math.max(4, maxEntries || 9);
    this._minEntries = Math.max(2, Math.ceil(this._maxEntries * 0.4));

    if (format) {
    this._initFormat(format);
    }

    this.clear();
    }

    rbush.prototype = {

    all: function () {
    return this._all(this.data, []);
    },

    search: function (bbox) {

    var node = this.data,
    result = [],
    toBBox = this.toBBox;

    if (!intersects(bbox, node.bbox)) return result;

    var nodesToSearch = [],
    i, len, child, childBBox;

    while (node) {
    for (i = 0, len = node.children.length; i < len; i++) {

    child = node.children[i];
    childBBox = node.leaf ? toBBox(child) : child.bbox;

    if (intersects(bbox, childBBox)) {
    if (node.leaf) result.push(child);
    else if (contains(bbox, childBBox)) this._all(child, result);
    else nodesToSearch.push(child);
    }
    }
    node = nodesToSearch.pop();
    }

    return result;
    },

    collides: function (bbox) {

    var node = this.data,
    toBBox = this.toBBox;

    if (!intersects(bbox, node.bbox)) return false;

    var nodesToSearch = [],
    i, len, child, childBBox;

    while (node) {
    for (i = 0, len = node.children.length; i < len; i++) {

    child = node.children[i];
    childBBox = node.leaf ? toBBox(child) : child.bbox;

    if (intersects(bbox, childBBox)) {
    if (node.leaf || contains(bbox, childBBox)) return true;
    nodesToSearch.push(child);
    }
    }
    node = nodesToSearch.pop();
    }

    return false;
    },

    load: function (data) {
    if (!(data && data.length)) return this;

    if (data.length < this._minEntries) {
    for (var i = 0, len = data.length; i < len; i++) {
    this.insert(data[i]);
    }
    return this;
    }

    // recursively build the tree with the given data from stratch using OMT algorithm
    var node = this._build(data.slice(), 0, data.length - 1, 0);

    if (!this.data.children.length) {
    // save as is if tree is empty
    this.data = node;

    } else if (this.data.height === node.height) {
    // split root if trees have the same height
    this._splitRoot(this.data, node);

    } else {
    if (this.data.height < node.height) {
    // swap trees if inserted one is bigger
    var tmpNode = this.data;
    this.data = node;
    node = tmpNode;
    }

    // insert the small tree into the large tree at appropriate level
    this._insert(node, this.data.height - node.height - 1, true);
    }

    return this;
    },

    insert: function (item) {
    if (item) this._insert(item, this.data.height - 1);
    return this;
    },

    clear: function () {
    this.data = {
    children: [],
    height: 1,
    bbox: empty(),
    leaf: true
    };
    return this;
    },

    remove: function (item) {
    if (!item) return this;

    var node = this.data,
    bbox = this.toBBox(item),
    path = [],
    indexes = [],
    i, parent, index, goingUp;

    // depth-first iterative tree traversal
    while (node || path.length) {

    if (!node) { // go up
    node = path.pop();
    parent = path[path.length - 1];
    i = indexes.pop();
    goingUp = true;
    }

    if (node.leaf) { // check current node
    index = node.children.indexOf(item);

    if (index !== -1) {
    // item found, remove the item and condense tree upwards
    node.children.splice(index, 1);
    path.push(node);
    this._condense(path);
    return this;
    }
    }

    if (!goingUp && !node.leaf && contains(node.bbox, bbox)) { // go down
    path.push(node);
    indexes.push(i);
    i = 0;
    parent = node;
    node = node.children[0];

    } else if (parent) { // go right
    i++;
    node = parent.children[i];
    goingUp = false;

    } else node = null; // nothing found
    }

    return this;
    },

    toBBox: function (item) { return item; },

    compareMinX: function (a, b) { return a[0] - b[0]; },
    compareMinY: function (a, b) { return a[1] - b[1]; },

    toJSON: function () { return this.data; },

    fromJSON: function (data) {
    this.data = data;
    return this;
    },

    _all: function (node, result) {
    var nodesToSearch = [];
    while (node) {
    if (node.leaf) result.push.apply(result, node.children);
    else nodesToSearch.push.apply(nodesToSearch, node.children);

    node = nodesToSearch.pop();
    }
    return result;
    },

    _build: function (items, left, right, height) {

    var N = right - left + 1,
    M = this._maxEntries,
    node;

    if (N <= M) {
    // reached leaf level; return leaf
    node = {
    children: items.slice(left, right + 1),
    height: 1,
    bbox: null,
    leaf: true
    };
    calcBBox(node, this.toBBox);
    return node;
    }

    if (!height) {
    // target height of the bulk-loaded tree
    height = Math.ceil(Math.log(N) / Math.log(M));

    // target number of root entries to maximize storage utilization
    M = Math.ceil(N / Math.pow(M, height - 1));
    }

    node = {
    children: [],
    height: height,
    bbox: null,
    leaf: false
    };

    // split the items into M mostly square tiles

    var N2 = Math.ceil(N / M),
    N1 = N2 * Math.ceil(Math.sqrt(M)),
    i, j, right2, right3;

    multiSelect(items, left, right, N1, this.compareMinX);

    for (i = left; i <= right; i += N1) {

    right2 = Math.min(i + N1 - 1, right);

    multiSelect(items, i, right2, N2, this.compareMinY);

    for (j = i; j <= right2; j += N2) {

    right3 = Math.min(j + N2 - 1, right2);

    // pack each entry recursively
    node.children.push(this._build(items, j, right3, height - 1));
    }
    }

    calcBBox(node, this.toBBox);

    return node;
    },

    _chooseSubtree: function (bbox, node, level, path) {

    var i, len, child, targetNode, area, enlargement, minArea, minEnlargement;

    while (true) {
    path.push(node);

    if (node.leaf || path.length - 1 === level) break;

    minArea = minEnlargement = Infinity;

    for (i = 0, len = node.children.length; i < len; i++) {
    child = node.children[i];
    area = bboxArea(child.bbox);
    enlargement = enlargedArea(bbox, child.bbox) - area;

    // choose entry with the least area enlargement
    if (enlargement < minEnlargement) {
    minEnlargement = enlargement;
    minArea = area < minArea ? area : minArea;
    targetNode = child;

    } else if (enlargement === minEnlargement) {
    // otherwise choose one with the smallest area
    if (area < minArea) {
    minArea = area;
    targetNode = child;
    }
    }
    }

    node = targetNode;
    }

    return node;
    },

    _insert: function (item, level, isNode) {

    var toBBox = this.toBBox,
    bbox = isNode ? item.bbox : toBBox(item),
    insertPath = [];

    // find the best node for accommodating the item, saving all nodes along the path too
    var node = this._chooseSubtree(bbox, this.data, level, insertPath);

    // put the item into the node
    node.children.push(item);
    extend(node.bbox, bbox);

    // split on node overflow; propagate upwards if necessary
    while (level >= 0) {
    if (insertPath[level].children.length > this._maxEntries) {
    this._split(insertPath, level);
    level--;
    } else break;
    }

    // adjust bboxes along the insertion path
    this._adjustParentBBoxes(bbox, insertPath, level);
    },

    // split overflowed node into two
    _split: function (insertPath, level) {

    var node = insertPath[level],
    M = node.children.length,
    m = this._minEntries;

    this._chooseSplitAxis(node, m, M);

    var splitIndex = this._chooseSplitIndex(node, m, M);

    var newNode = {
    children: node.children.splice(splitIndex, node.children.length - splitIndex),
    height: node.height,
    bbox: null,
    leaf: false
    };

    if (node.leaf) newNode.leaf = true;

    calcBBox(node, this.toBBox);
    calcBBox(newNode, this.toBBox);

    if (level) insertPath[level - 1].children.push(newNode);
    else this._splitRoot(node, newNode);
    },

    _splitRoot: function (node, newNode) {
    // split root node
    this.data = {
    children: [node, newNode],
    height: node.height + 1,
    bbox: null,
    leaf: false
    };
    calcBBox(this.data, this.toBBox);
    },

    _chooseSplitIndex: function (node, m, M) {

    var i, bbox1, bbox2, overlap, area, minOverlap, minArea, index;

    minOverlap = minArea = Infinity;

    for (i = m; i <= M - m; i++) {
    bbox1 = distBBox(node, 0, i, this.toBBox);
    bbox2 = distBBox(node, i, M, this.toBBox);

    overlap = intersectionArea(bbox1, bbox2);
    area = bboxArea(bbox1) + bboxArea(bbox2);

    // choose distribution with minimum overlap
    if (overlap < minOverlap) {
    minOverlap = overlap;
    index = i;

    minArea = area < minArea ? area : minArea;

    } else if (overlap === minOverlap) {
    // otherwise choose distribution with minimum area
    if (area < minArea) {
    minArea = area;
    index = i;
    }
    }
    }

    return index;
    },

    // sorts node children by the best axis for split
    _chooseSplitAxis: function (node, m, M) {

    var compareMinX = node.leaf ? this.compareMinX : compareNodeMinX,
    compareMinY = node.leaf ? this.compareMinY : compareNodeMinY,
    xMargin = this._allDistMargin(node, m, M, compareMinX),
    yMargin = this._allDistMargin(node, m, M, compareMinY);

    // if total distributions margin value is minimal for x, sort by minX,
    // otherwise it's already sorted by minY
    if (xMargin < yMargin) node.children.sort(compareMinX);
    },

    // total margin of all possible split distributions where each node is at least m full
    _allDistMargin: function (node, m, M, compare) {

    node.children.sort(compare);

    var toBBox = this.toBBox,
    leftBBox = distBBox(node, 0, m, toBBox),
    rightBBox = distBBox(node, M - m, M, toBBox),
    margin = bboxMargin(leftBBox) + bboxMargin(rightBBox),
    i, child;

    for (i = m; i < M - m; i++) {
    child = node.children[i];
    extend(leftBBox, node.leaf ? toBBox(child) : child.bbox);
    margin += bboxMargin(leftBBox);
    }

    for (i = M - m - 1; i >= m; i--) {
    child = node.children[i];
    extend(rightBBox, node.leaf ? toBBox(child) : child.bbox);
    margin += bboxMargin(rightBBox);
    }

    return margin;
    },

    _adjustParentBBoxes: function (bbox, path, level) {
    // adjust bboxes along the given tree path
    for (var i = level; i >= 0; i--) {
    extend(path[i].bbox, bbox);
    }
    },

    _condense: function (path) {
    // go through the path, removing empty nodes and updating bboxes
    for (var i = path.length - 1, siblings; i >= 0; i--) {
    if (path[i].children.length === 0) {
    if (i > 0) {
    siblings = path[i - 1].children;
    siblings.splice(siblings.indexOf(path[i]), 1);

    } else this.clear();

    } else calcBBox(path[i], this.toBBox);
    }
    },

    _initFormat: function (format) {
    // data format (minX, minY, maxX, maxY accessors)

    // uses eval-type function compilation instead of just accepting a toBBox function
    // because the algorithms are very sensitive to sorting functions performance,
    // so they should be dead simple and without inner calls

    // jshint evil: true

    var compareArr = ['return a', ' - b', ';'];

    this.compareMinX = new Function('a', 'b', compareArr.join(format[0]));
    this.compareMinY = new Function('a', 'b', compareArr.join(format[1]));

    this.toBBox = new Function('a', 'return [a' + format.join(', a') + '];');
    }
    };


    // calculate node's bbox from bboxes of its children
    function calcBBox(node, toBBox) {
    node.bbox = distBBox(node, 0, node.children.length, toBBox);
    }

    // min bounding rectangle of node children from k to p-1
    function distBBox(node, k, p, toBBox) {
    var bbox = empty();

    for (var i = k, child; i < p; i++) {
    child = node.children[i];
    extend(bbox, node.leaf ? toBBox(child) : child.bbox);
    }

    return bbox;
    }

    function empty() { return [Infinity, Infinity, -Infinity, -Infinity]; }

    function extend(a, b) {
    a[0] = Math.min(a[0], b[0]);
    a[1] = Math.min(a[1], b[1]);
    a[2] = Math.max(a[2], b[2]);
    a[3] = Math.max(a[3], b[3]);
    return a;
    }

    function compareNodeMinX(a, b) { return a.bbox[0] - b.bbox[0]; }
    function compareNodeMinY(a, b) { return a.bbox[1] - b.bbox[1]; }

    function bboxArea(a) { return (a[2] - a[0]) * (a[3] - a[1]); }
    function bboxMargin(a) { return (a[2] - a[0]) + (a[3] - a[1]); }

    function enlargedArea(a, b) {
    return (Math.max(b[2], a[2]) - Math.min(b[0], a[0])) *
    (Math.max(b[3], a[3]) - Math.min(b[1], a[1]));
    }

    function intersectionArea(a, b) {
    var minX = Math.max(a[0], b[0]),
    minY = Math.max(a[1], b[1]),
    maxX = Math.min(a[2], b[2]),
    maxY = Math.min(a[3], b[3]);

    return Math.max(0, maxX - minX) *
    Math.max(0, maxY - minY);
    }

    function contains(a, b) {
    return a[0] <= b[0] &&
    a[1] <= b[1] &&
    b[2] <= a[2] &&
    b[3] <= a[3];
    }

    function intersects(a, b) {
    return b[0] <= a[2] &&
    b[1] <= a[3] &&
    b[2] >= a[0] &&
    b[3] >= a[1];
    }

    // sort an array so that items come in groups of n unsorted items, with groups sorted between each other;
    // combines selection algorithm with binary divide & conquer approach

    function multiSelect(arr, left, right, n, compare) {
    var stack = [left, right],
    mid;

    while (stack.length) {
    right = stack.pop();
    left = stack.pop();

    if (right - left <= n) continue;

    mid = left + Math.ceil((right - left) / n / 2) * n;
    select(arr, left, right, mid, compare);

    stack.push(left, mid, mid, right);
    }
    }

    // Floyd-Rivest selection algorithm:
    // sort an array between left and right (inclusive) so that the smallest k elements come first (unordered)
    function select(arr, left, right, k, compare) {
    var n, i, z, s, sd, newLeft, newRight, t, j;

    while (right > left) {
    if (right - left > 600) {
    n = right - left + 1;
    i = k - left + 1;
    z = Math.log(n);
    s = 0.5 * Math.exp(2 * z / 3);
    sd = 0.5 * Math.sqrt(z * s * (n - s) / n) * (i - n / 2 < 0 ? -1 : 1);
    newLeft = Math.max(left, Math.floor(k - i * s / n + sd));
    newRight = Math.min(right, Math.floor(k + (n - i) * s / n + sd));
    select(arr, newLeft, newRight, k, compare);
    }

    t = arr[k];
    i = left;
    j = right;

    swap(arr, left, k);
    if (compare(arr[right], t) > 0) swap(arr, left, right);

    while (i < j) {
    swap(arr, i, j);
    i++;
    j--;
    while (compare(arr[i], t) < 0) i++;
    while (compare(arr[j], t) > 0) j--;
    }

    if (compare(arr[left], t) === 0) swap(arr, left, j);
    else {
    j++;
    swap(arr, j, right);
    }

    if (j <= k) left = j + 1;
    if (k <= j) right = j - 1;
    }
    }

    function swap(arr, i, j) {
    var tmp = arr[i];
    arr[i] = arr[j];
    arr[j] = tmp;
    }


    // export as AMD/CommonJS module or global variable
    if (typeof define === 'function' && define.amd) define('rbush', function () { return rbush; });
    else if (typeof module !== 'undefined') module.exports = rbush;
    else if (typeof self !== 'undefined') self.rbush = rbush;
    else window.rbush = rbush;

    })();
    1 change: 1 addition & 0 deletions rbush.min.js
    Original file line number Diff line number Diff line change
    @@ -0,0 +1 @@
    (function(){"use strict";function rbush(maxEntries,format){if(!(this instanceof rbush))return new rbush(maxEntries,format);this._maxEntries=Math.max(4,maxEntries||9);this._minEntries=Math.max(2,Math.ceil(this._maxEntries*.4));if(format){this._initFormat(format)}this.clear()}rbush.prototype={all:function(){return this._all(this.data,[])},search:function(bbox){var node=this.data,result=[],toBBox=this.toBBox;if(!intersects(bbox,node.bbox))return result;var nodesToSearch=[],i,len,child,childBBox;while(node){for(i=0,len=node.children.length;i<len;i++){child=node.children[i];childBBox=node.leaf?toBBox(child):child.bbox;if(intersects(bbox,childBBox)){if(node.leaf)result.push(child);else if(contains(bbox,childBBox))this._all(child,result);else nodesToSearch.push(child)}}node=nodesToSearch.pop()}return result},collides:function(bbox){var node=this.data,toBBox=this.toBBox;if(!intersects(bbox,node.bbox))return false;var nodesToSearch=[],i,len,child,childBBox;while(node){for(i=0,len=node.children.length;i<len;i++){child=node.children[i];childBBox=node.leaf?toBBox(child):child.bbox;if(intersects(bbox,childBBox)){if(node.leaf||contains(bbox,childBBox))return true;nodesToSearch.push(child)}}node=nodesToSearch.pop()}return false},load:function(data){if(!(data&&data.length))return this;if(data.length<this._minEntries){for(var i=0,len=data.length;i<len;i++){this.insert(data[i])}return this}var node=this._build(data.slice(),0,data.length-1,0);if(!this.data.children.length){this.data=node}else if(this.data.height===node.height){this._splitRoot(this.data,node)}else{if(this.data.height<node.height){var tmpNode=this.data;this.data=node;node=tmpNode}this._insert(node,this.data.height-node.height-1,true)}return this},insert:function(item){if(item)this._insert(item,this.data.height-1);return this},clear:function(){this.data={children:[],height:1,bbox:empty(),leaf:true};return this},remove:function(item){if(!item)return this;var node=this.data,bbox=this.toBBox(item),path=[],indexes=[],i,parent,index,goingUp;while(node||path.length){if(!node){node=path.pop();parent=path[path.length-1];i=indexes.pop();goingUp=true}if(node.leaf){index=node.children.indexOf(item);if(index!==-1){node.children.splice(index,1);path.push(node);this._condense(path);return this}}if(!goingUp&&!node.leaf&&contains(node.bbox,bbox)){path.push(node);indexes.push(i);i=0;parent=node;node=node.children[0]}else if(parent){i++;node=parent.children[i];goingUp=false}else node=null}return this},toBBox:function(item){return item},compareMinX:function(a,b){return a[0]-b[0]},compareMinY:function(a,b){return a[1]-b[1]},toJSON:function(){return this.data},fromJSON:function(data){this.data=data;return this},_all:function(node,result){var nodesToSearch=[];while(node){if(node.leaf)result.push.apply(result,node.children);else nodesToSearch.push.apply(nodesToSearch,node.children);node=nodesToSearch.pop()}return result},_build:function(items,left,right,height){var N=right-left+1,M=this._maxEntries,node;if(N<=M){node={children:items.slice(left,right+1),height:1,bbox:null,leaf:true};calcBBox(node,this.toBBox);return node}if(!height){height=Math.ceil(Math.log(N)/Math.log(M));M=Math.ceil(N/Math.pow(M,height-1))}node={children:[],height:height,bbox:null,leaf:false};var N2=Math.ceil(N/M),N1=N2*Math.ceil(Math.sqrt(M)),i,j,right2,right3;multiSelect(items,left,right,N1,this.compareMinX);for(i=left;i<=right;i+=N1){right2=Math.min(i+N1-1,right);multiSelect(items,i,right2,N2,this.compareMinY);for(j=i;j<=right2;j+=N2){right3=Math.min(j+N2-1,right2);node.children.push(this._build(items,j,right3,height-1))}}calcBBox(node,this.toBBox);return node},_chooseSubtree:function(bbox,node,level,path){var i,len,child,targetNode,area,enlargement,minArea,minEnlargement;while(true){path.push(node);if(node.leaf||path.length-1===level)break;minArea=minEnlargement=Infinity;for(i=0,len=node.children.length;i<len;i++){child=node.children[i];area=bboxArea(child.bbox);enlargement=enlargedArea(bbox,child.bbox)-area;if(enlargement<minEnlargement){minEnlargement=enlargement;minArea=area<minArea?area:minArea;targetNode=child}else if(enlargement===minEnlargement){if(area<minArea){minArea=area;targetNode=child}}}node=targetNode}return node},_insert:function(item,level,isNode){var toBBox=this.toBBox,bbox=isNode?item.bbox:toBBox(item),insertPath=[];var node=this._chooseSubtree(bbox,this.data,level,insertPath);node.children.push(item);extend(node.bbox,bbox);while(level>=0){if(insertPath[level].children.length>this._maxEntries){this._split(insertPath,level);level--}else break}this._adjustParentBBoxes(bbox,insertPath,level)},_split:function(insertPath,level){var node=insertPath[level],M=node.children.length,m=this._minEntries;this._chooseSplitAxis(node,m,M);var splitIndex=this._chooseSplitIndex(node,m,M);var newNode={children:node.children.splice(splitIndex,node.children.length-splitIndex),height:node.height,bbox:null,leaf:false};if(node.leaf)newNode.leaf=true;calcBBox(node,this.toBBox);calcBBox(newNode,this.toBBox);if(level)insertPath[level-1].children.push(newNode);else this._splitRoot(node,newNode)},_splitRoot:function(node,newNode){this.data={children:[node,newNode],height:node.height+1,bbox:null,leaf:false};calcBBox(this.data,this.toBBox)},_chooseSplitIndex:function(node,m,M){var i,bbox1,bbox2,overlap,area,minOverlap,minArea,index;minOverlap=minArea=Infinity;for(i=m;i<=M-m;i++){bbox1=distBBox(node,0,i,this.toBBox);bbox2=distBBox(node,i,M,this.toBBox);overlap=intersectionArea(bbox1,bbox2);area=bboxArea(bbox1)+bboxArea(bbox2);if(overlap<minOverlap){minOverlap=overlap;index=i;minArea=area<minArea?area:minArea}else if(overlap===minOverlap){if(area<minArea){minArea=area;index=i}}}return index},_chooseSplitAxis:function(node,m,M){var compareMinX=node.leaf?this.compareMinX:compareNodeMinX,compareMinY=node.leaf?this.compareMinY:compareNodeMinY,xMargin=this._allDistMargin(node,m,M,compareMinX),yMargin=this._allDistMargin(node,m,M,compareMinY);if(xMargin<yMargin)node.children.sort(compareMinX)},_allDistMargin:function(node,m,M,compare){node.children.sort(compare);var toBBox=this.toBBox,leftBBox=distBBox(node,0,m,toBBox),rightBBox=distBBox(node,M-m,M,toBBox),margin=bboxMargin(leftBBox)+bboxMargin(rightBBox),i,child;for(i=m;i<M-m;i++){child=node.children[i];extend(leftBBox,node.leaf?toBBox(child):child.bbox);margin+=bboxMargin(leftBBox)}for(i=M-m-1;i>=m;i--){child=node.children[i];extend(rightBBox,node.leaf?toBBox(child):child.bbox);margin+=bboxMargin(rightBBox)}return margin},_adjustParentBBoxes:function(bbox,path,level){for(var i=level;i>=0;i--){extend(path[i].bbox,bbox)}},_condense:function(path){for(var i=path.length-1,siblings;i>=0;i--){if(path[i].children.length===0){if(i>0){siblings=path[i-1].children;siblings.splice(siblings.indexOf(path[i]),1)}else this.clear()}else calcBBox(path[i],this.toBBox)}},_initFormat:function(format){var compareArr=["return a"," - b",";"];this.compareMinX=new Function("a","b",compareArr.join(format[0]));this.compareMinY=new Function("a","b",compareArr.join(format[1]));this.toBBox=new Function("a","return [a"+format.join(", a")+"];")}};function calcBBox(node,toBBox){node.bbox=distBBox(node,0,node.children.length,toBBox)}function distBBox(node,k,p,toBBox){var bbox=empty();for(var i=k,child;i<p;i++){child=node.children[i];extend(bbox,node.leaf?toBBox(child):child.bbox)}return bbox}function empty(){return[Infinity,Infinity,-Infinity,-Infinity]}function extend(a,b){a[0]=Math.min(a[0],b[0]);a[1]=Math.min(a[1],b[1]);a[2]=Math.max(a[2],b[2]);a[3]=Math.max(a[3],b[3]);return a}function compareNodeMinX(a,b){return a.bbox[0]-b.bbox[0]}function compareNodeMinY(a,b){return a.bbox[1]-b.bbox[1]}function bboxArea(a){return(a[2]-a[0])*(a[3]-a[1])}function bboxMargin(a){return a[2]-a[0]+(a[3]-a[1])}function enlargedArea(a,b){return(Math.max(b[2],a[2])-Math.min(b[0],a[0]))*(Math.max(b[3],a[3])-Math.min(b[1],a[1]))}function intersectionArea(a,b){var minX=Math.max(a[0],b[0]),minY=Math.max(a[1],b[1]),maxX=Math.min(a[2],b[2]),maxY=Math.min(a[3],b[3]);return Math.max(0,maxX-minX)*Math.max(0,maxY-minY)}function contains(a,b){return a[0]<=b[0]&&a[1]<=b[1]&&b[2]<=a[2]&&b[3]<=a[3]}function intersects(a,b){return b[0]<=a[2]&&b[1]<=a[3]&&b[2]>=a[0]&&b[3]>=a[1]}function multiSelect(arr,left,right,n,compare){var stack=[left,right],mid;while(stack.length){right=stack.pop();left=stack.pop();if(right-left<=n)continue;mid=left+Math.ceil((right-left)/n/2)*n;select(arr,left,right,mid,compare);stack.push(left,mid,mid,right)}}function select(arr,left,right,k,compare){var n,i,z,s,sd,newLeft,newRight,t,j;while(right>left){if(right-left>600){n=right-left+1;i=k-left+1;z=Math.log(n);s=.5*Math.exp(2*z/3);sd=.5*Math.sqrt(z*s*(n-s)/n)*(i-n/2<0?-1:1);newLeft=Math.max(left,Math.floor(k-i*s/n+sd));newRight=Math.min(right,Math.floor(k+(n-i)*s/n+sd));select(arr,newLeft,newRight,k,compare)}t=arr[k];i=left;j=right;swap(arr,left,k);if(compare(arr[right],t)>0)swap(arr,left,right);while(i<j){swap(arr,i,j);i++;j--;while(compare(arr[i],t)<0)i++;while(compare(arr[j],t)>0)j--}if(compare(arr[left],t)===0)swap(arr,left,j);else{j++;swap(arr,j,right)}if(j<=k)left=j+1;if(k<=j)right=j-1}}function swap(arr,i,j){var tmp=arr[i];arr[i]=arr[j];arr[j]=tmp}if(typeof define==="function"&&define.amd)define("rbush",function(){return rbush});else if(typeof module!=="undefined")module.exports=rbush;else if(typeof self!=="undefined")self.rbush=rbush;else window.rbush=rbush})();
    4 changes: 2 additions & 2 deletions spam.js
    Original file line number Diff line number Diff line change
    @@ -207,7 +207,7 @@ var ZoomableCanvasMap;
    }, parameters),
    simplify = d3.geo.transform({
    point: function(x, y, z) {
    if (z >= settings.area) {
    if (!z || z >= settings.area) {
    this.stream.point(x, y)
    }
    }
    @@ -244,7 +244,7 @@ var ZoomableCanvasMap;
    settings.projection.scale(0.9 * (settings.width / dx))
    .translate([settings.width / 2, settings.height / 2])
    } else if (!settings.height) {
    settings.height = dy * 1 / 0.9
    settings.height = Math.ceil(dy * 1 / 0.9)
    }
    d3.select(settings.parameters).attr("height", settings.height)

    Binary file modified thumbnail.png
    Loading
    Sorry, something went wrong. Reload?
    Sorry, we cannot display this file.
    Sorry, this file is invalid so it cannot be displayed.
    538 changes: 0 additions & 538 deletions topojson.js
    Original file line number Diff line number Diff line change
    @@ -1,538 +0,0 @@
    !function() {
    var topojson = {
    version: "1.6.19",
    mesh: function(topology) { return object(topology, meshArcs.apply(this, arguments)); },
    meshArcs: meshArcs,
    merge: function(topology) { return object(topology, mergeArcs.apply(this, arguments)); },
    mergeArcs: mergeArcs,
    feature: featureOrCollection,
    neighbors: neighbors,
    presimplify: presimplify
    };

    function stitchArcs(topology, arcs) {
    var stitchedArcs = {},
    fragmentByStart = {},
    fragmentByEnd = {},
    fragments = [],
    emptyIndex = -1;

    // Stitch empty arcs first, since they may be subsumed by other arcs.
    arcs.forEach(function(i, j) {
    var arc = topology.arcs[i < 0 ? ~i : i], t;
    if (arc.length < 3 && !arc[1][0] && !arc[1][1]) {
    t = arcs[++emptyIndex], arcs[emptyIndex] = i, arcs[j] = t;
    }
    });

    arcs.forEach(function(i) {
    var e = ends(i),
    start = e[0],
    end = e[1],
    f, g;

    if (f = fragmentByEnd[start]) {
    delete fragmentByEnd[f.end];
    f.push(i);
    f.end = end;
    if (g = fragmentByStart[end]) {
    delete fragmentByStart[g.start];
    var fg = g === f ? f : f.concat(g);
    fragmentByStart[fg.start = f.start] = fragmentByEnd[fg.end = g.end] = fg;
    } else {
    fragmentByStart[f.start] = fragmentByEnd[f.end] = f;
    }
    } else if (f = fragmentByStart[end]) {
    delete fragmentByStart[f.start];
    f.unshift(i);
    f.start = start;
    if (g = fragmentByEnd[start]) {
    delete fragmentByEnd[g.end];
    var gf = g === f ? f : g.concat(f);
    fragmentByStart[gf.start = g.start] = fragmentByEnd[gf.end = f.end] = gf;
    } else {
    fragmentByStart[f.start] = fragmentByEnd[f.end] = f;
    }
    } else {
    f = [i];
    fragmentByStart[f.start = start] = fragmentByEnd[f.end = end] = f;
    }
    });

    function ends(i) {
    var arc = topology.arcs[i < 0 ? ~i : i], p0 = arc[0], p1;
    if (topology.transform) p1 = [0, 0], arc.forEach(function(dp) { p1[0] += dp[0], p1[1] += dp[1]; });
    else p1 = arc[arc.length - 1];
    return i < 0 ? [p1, p0] : [p0, p1];
    }

    function flush(fragmentByEnd, fragmentByStart) {
    for (var k in fragmentByEnd) {
    var f = fragmentByEnd[k];
    delete fragmentByStart[f.start];
    delete f.start;
    delete f.end;
    f.forEach(function(i) { stitchedArcs[i < 0 ? ~i : i] = 1; });
    fragments.push(f);
    }
    }

    flush(fragmentByEnd, fragmentByStart);
    flush(fragmentByStart, fragmentByEnd);
    arcs.forEach(function(i) { if (!stitchedArcs[i < 0 ? ~i : i]) fragments.push([i]); });

    return fragments;
    }

    function meshArcs(topology, o, filter) {
    var arcs = [];

    if (arguments.length > 1) {
    var geomsByArc = [],
    geom;

    function arc(i) {
    var j = i < 0 ? ~i : i;
    (geomsByArc[j] || (geomsByArc[j] = [])).push({i: i, g: geom});
    }

    function line(arcs) {
    arcs.forEach(arc);
    }

    function polygon(arcs) {
    arcs.forEach(line);
    }

    function geometry(o) {
    if (o.type === "GeometryCollection") o.geometries.forEach(geometry);
    else if (o.type in geometryType) geom = o, geometryType[o.type](o.arcs);
    }

    var geometryType = {
    LineString: line,
    MultiLineString: polygon,
    Polygon: polygon,
    MultiPolygon: function(arcs) { arcs.forEach(polygon); }
    };

    geometry(o);

    geomsByArc.forEach(arguments.length < 3
    ? function(geoms) { arcs.push(geoms[0].i); }
    : function(geoms) { if (filter(geoms[0].g, geoms[geoms.length - 1].g)) arcs.push(geoms[0].i); });
    } else {
    for (var i = 0, n = topology.arcs.length; i < n; ++i) arcs.push(i);
    }

    return {type: "MultiLineString", arcs: stitchArcs(topology, arcs)};
    }

    function mergeArcs(topology, objects) {
    var polygonsByArc = {},
    polygons = [],
    components = [];

    objects.forEach(function(o) {
    if (o.type === "Polygon") register(o.arcs);
    else if (o.type === "MultiPolygon") o.arcs.forEach(register);
    });

    function register(polygon) {
    polygon.forEach(function(ring) {
    ring.forEach(function(arc) {
    (polygonsByArc[arc = arc < 0 ? ~arc : arc] || (polygonsByArc[arc] = [])).push(polygon);
    });
    });
    polygons.push(polygon);
    }

    function exterior(ring) {
    return cartesianRingArea(object(topology, {type: "Polygon", arcs: [ring]}).coordinates[0]) > 0; // TODO allow spherical?
    }

    polygons.forEach(function(polygon) {
    if (!polygon._) {
    var component = [],
    neighbors = [polygon];
    polygon._ = 1;
    components.push(component);
    while (polygon = neighbors.pop()) {
    component.push(polygon);
    polygon.forEach(function(ring) {
    ring.forEach(function(arc) {
    polygonsByArc[arc < 0 ? ~arc : arc].forEach(function(polygon) {
    if (!polygon._) {
    polygon._ = 1;
    neighbors.push(polygon);
    }
    });
    });
    });
    }
    }
    });

    polygons.forEach(function(polygon) {
    delete polygon._;
    });

    return {
    type: "MultiPolygon",
    arcs: components.map(function(polygons) {
    var arcs = [];

    // Extract the exterior (unique) arcs.
    polygons.forEach(function(polygon) {
    polygon.forEach(function(ring) {
    ring.forEach(function(arc) {
    if (polygonsByArc[arc < 0 ? ~arc : arc].length < 2) {
    arcs.push(arc);
    }
    });
    });
    });

    // Stitch the arcs into one or more rings.
    arcs = stitchArcs(topology, arcs);

    // If more than one ring is returned,
    // at most one of these rings can be the exterior;
    // this exterior ring has the same winding order
    // as any exterior ring in the original polygons.
    if ((n = arcs.length) > 1) {
    var sgn = exterior(polygons[0][0]);
    for (var i = 0, t; i < n; ++i) {
    if (sgn === exterior(arcs[i])) {
    t = arcs[0], arcs[0] = arcs[i], arcs[i] = t;
    break;
    }
    }
    }

    return arcs;
    })
    };
    }

    function featureOrCollection(topology, o) {
    return o.type === "GeometryCollection" ? {
    type: "FeatureCollection",
    features: o.geometries.map(function(o) { return feature(topology, o); })
    } : feature(topology, o);
    }

    function feature(topology, o) {
    var f = {
    type: "Feature",
    id: o.id,
    properties: o.properties || {},
    geometry: object(topology, o)
    };
    if (o.id == null) delete f.id;
    return f;
    }

    function object(topology, o) {
    var absolute = transformAbsolute(topology.transform),
    arcs = topology.arcs;

    function arc(i, points) {
    if (points.length) points.pop();
    for (var a = arcs[i < 0 ? ~i : i], k = 0, n = a.length, p; k < n; ++k) {
    points.push(p = a[k].slice());
    absolute(p, k);
    }
    if (i < 0) reverse(points, n);
    }

    function point(p) {
    p = p.slice();
    absolute(p, 0);
    return p;
    }

    function line(arcs) {
    var points = [];
    for (var i = 0, n = arcs.length; i < n; ++i) arc(arcs[i], points);
    if (points.length < 2) points.push(points[0].slice());
    return points;
    }

    function ring(arcs) {
    var points = line(arcs);
    while (points.length < 4) points.push(points[0].slice());
    return points;
    }

    function polygon(arcs) {
    return arcs.map(ring);
    }

    function geometry(o) {
    var t = o.type;
    return t === "GeometryCollection" ? {type: t, geometries: o.geometries.map(geometry)}
    : t in geometryType ? {type: t, coordinates: geometryType[t](o)}
    : null;
    }

    var geometryType = {
    Point: function(o) { return point(o.coordinates); },
    MultiPoint: function(o) { return o.coordinates.map(point); },
    LineString: function(o) { return line(o.arcs); },
    MultiLineString: function(o) { return o.arcs.map(line); },
    Polygon: function(o) { return polygon(o.arcs); },
    MultiPolygon: function(o) { return o.arcs.map(polygon); }
    };

    return geometry(o);
    }

    function reverse(array, n) {
    var t, j = array.length, i = j - n; while (i < --j) t = array[i], array[i++] = array[j], array[j] = t;
    }

    function bisect(a, x) {
    var lo = 0, hi = a.length;
    while (lo < hi) {
    var mid = lo + hi >>> 1;
    if (a[mid] < x) lo = mid + 1;
    else hi = mid;
    }
    return lo;
    }

    function neighbors(objects) {
    var indexesByArc = {}, // arc index -> array of object indexes
    neighbors = objects.map(function() { return []; });

    function line(arcs, i) {
    arcs.forEach(function(a) {
    if (a < 0) a = ~a;
    var o = indexesByArc[a];
    if (o) o.push(i);
    else indexesByArc[a] = [i];
    });
    }

    function polygon(arcs, i) {
    arcs.forEach(function(arc) { line(arc, i); });
    }

    function geometry(o, i) {
    if (o.type === "GeometryCollection") o.geometries.forEach(function(o) { geometry(o, i); });
    else if (o.type in geometryType) geometryType[o.type](o.arcs, i);
    }

    var geometryType = {
    LineString: line,
    MultiLineString: polygon,
    Polygon: polygon,
    MultiPolygon: function(arcs, i) { arcs.forEach(function(arc) { polygon(arc, i); }); }
    };

    objects.forEach(geometry);

    for (var i in indexesByArc) {
    for (var indexes = indexesByArc[i], m = indexes.length, j = 0; j < m; ++j) {
    for (var k = j + 1; k < m; ++k) {
    var ij = indexes[j], ik = indexes[k], n;
    if ((n = neighbors[ij])[i = bisect(n, ik)] !== ik) n.splice(i, 0, ik);
    if ((n = neighbors[ik])[i = bisect(n, ij)] !== ij) n.splice(i, 0, ij);
    }
    }
    }

    return neighbors;
    }

    function presimplify(topology, triangleArea) {
    var absolute = transformAbsolute(topology.transform),
    relative = transformRelative(topology.transform),
    heap = minAreaHeap();

    if (!triangleArea) triangleArea = cartesianTriangleArea;

    topology.arcs.forEach(function(arc) {
    var triangles = [],
    maxArea = 0,
    triangle;

    // To store each point’s effective area, we create a new array rather than
    // extending the passed-in point to workaround a Chrome/V8 bug (getting
    // stuck in smi mode). For midpoints, the initial effective area of
    // Infinity will be computed in the next step.
    for (var i = 0, n = arc.length, p; i < n; ++i) {
    p = arc[i];
    arc[i] = [p[0], p[1], Infinity, p[0], p[1]]
    absolute(arc[i], i);
    }

    for (var i = 1, n = arc.length - 1; i < n; ++i) {
    triangle = arc.slice(i - 1, i + 2);
    triangle[1][2] = triangleArea(triangle);
    triangles.push(triangle);
    heap.push(triangle);
    }

    for (var i = 0, n = triangles.length; i < n; ++i) {
    triangle = triangles[i];
    triangle.previous = triangles[i - 1];
    triangle.next = triangles[i + 1];
    }

    while (triangle = heap.pop()) {
    var previous = triangle.previous,
    next = triangle.next;

    // If the area of the current point is less than that of the previous point
    // to be eliminated, use the latter's area instead. This ensures that the
    // current point cannot be eliminated without eliminating previously-
    // eliminated points.
    if (triangle[1][2] < maxArea) triangle[1][2] = maxArea;
    else maxArea = triangle[1][2];

    if (previous) {
    previous.next = next;
    previous[2] = triangle[2];
    update(previous);
    }

    if (next) {
    next.previous = previous;
    next[0] = triangle[0];
    update(next);
    }
    }

    for (var i = 0, n = arc.length, p; i < n; ++i) {
    p = arc[i];
    arc[i] = [p[3], p[4], p[2]]
    }
    });

    function update(triangle) {
    heap.remove(triangle);
    triangle[1][2] = triangleArea(triangle);
    heap.push(triangle);
    }

    return topology;
    };

    function cartesianRingArea(ring) {
    var i = -1,
    n = ring.length,
    a,
    b = ring[n - 1],
    area = 0;

    while (++i < n) {
    a = b;
    b = ring[i];
    area += a[0] * b[1] - a[1] * b[0];
    }

    return area * .5;
    }

    function cartesianTriangleArea(triangle) {
    var a = triangle[0], b = triangle[1], c = triangle[2];
    return Math.abs((a[0] - c[0]) * (b[1] - a[1]) - (a[0] - b[0]) * (c[1] - a[1]));
    }

    function compareArea(a, b) {
    return a[1][2] - b[1][2];
    }

    function minAreaHeap() {
    var heap = {},
    array = [],
    size = 0;

    heap.push = function(object) {
    up(array[object._ = size] = object, size++);
    return size;
    };

    heap.pop = function() {
    if (size <= 0) return;
    var removed = array[0], object;
    if (--size > 0) object = array[size], down(array[object._ = 0] = object, 0);
    return removed;
    };

    heap.remove = function(removed) {
    var i = removed._, object;
    if (array[i] !== removed) return; // invalid request
    if (i !== --size) object = array[size], (compareArea(object, removed) < 0 ? up : down)(array[object._ = i] = object, i);
    return i;
    };

    function up(object, i) {
    while (i > 0) {
    var j = ((i + 1) >> 1) - 1,
    parent = array[j];
    if (compareArea(object, parent) >= 0) break;
    array[parent._ = i] = parent;
    array[object._ = i = j] = object;
    }
    }

    function down(object, i) {
    while (true) {
    var r = (i + 1) << 1,
    l = r - 1,
    j = i,
    child = array[j];
    if (l < size && compareArea(array[l], child) < 0) child = array[j = l];
    if (r < size && compareArea(array[r], child) < 0) child = array[j = r];
    if (j === i) break;
    array[child._ = i] = child;
    array[object._ = i = j] = object;
    }
    }

    return heap;
    }

    function transformAbsolute(transform) {
    if (!transform) return noop;
    var x0,
    y0,
    kx = transform.scale[0],
    ky = transform.scale[1],
    dx = transform.translate[0],
    dy = transform.translate[1];
    return function(point, i) {
    if (!i) x0 = y0 = 0;
    point[0] = (x0 += point[0]) * kx + dx;
    point[1] = (y0 += point[1]) * ky + dy;
    };
    }

    function transformRelative(transform) {
    if (!transform) return noop;
    var x0,
    y0,
    kx = transform.scale[0],
    ky = transform.scale[1],
    dx = transform.translate[0],
    dy = transform.translate[1];
    return function(point, i) {
    if (!i) x0 = y0 = 0;
    var x1 = (point[0] - dx) / kx | 0,
    y1 = (point[1] - dy) / ky | 0;
    point[0] = x1 - x0;
    point[1] = y1 - y0;
    x0 = x1;
    y0 = y1;
    };
    }

    function noop() {}

    if (typeof define === "function" && define.amd) define(topojson);
    else if (typeof module === "object" && module.exports) module.exports = topojson;
    else this.topojson = topojson;
    }();
    1 change: 1 addition & 0 deletions topojson.min.js
    Original file line number Diff line number Diff line change
    @@ -0,0 +1 @@
    !function(){var topojson={version:"1.6.19",mesh:function(topology){return object(topology,meshArcs.apply(this,arguments))},meshArcs:meshArcs,merge:function(topology){return object(topology,mergeArcs.apply(this,arguments))},mergeArcs:mergeArcs,feature:featureOrCollection,neighbors:neighbors,presimplify:presimplify};function stitchArcs(topology,arcs){var stitchedArcs={},fragmentByStart={},fragmentByEnd={},fragments=[],emptyIndex=-1;arcs.forEach(function(i,j){var arc=topology.arcs[i<0?~i:i],t;if(arc.length<3&&!arc[1][0]&&!arc[1][1]){t=arcs[++emptyIndex],arcs[emptyIndex]=i,arcs[j]=t}});arcs.forEach(function(i){var e=ends(i),start=e[0],end=e[1],f,g;if(f=fragmentByEnd[start]){delete fragmentByEnd[f.end];f.push(i);f.end=end;if(g=fragmentByStart[end]){delete fragmentByStart[g.start];var fg=g===f?f:f.concat(g);fragmentByStart[fg.start=f.start]=fragmentByEnd[fg.end=g.end]=fg}else{fragmentByStart[f.start]=fragmentByEnd[f.end]=f}}else if(f=fragmentByStart[end]){delete fragmentByStart[f.start];f.unshift(i);f.start=start;if(g=fragmentByEnd[start]){delete fragmentByEnd[g.end];var gf=g===f?f:g.concat(f);fragmentByStart[gf.start=g.start]=fragmentByEnd[gf.end=f.end]=gf}else{fragmentByStart[f.start]=fragmentByEnd[f.end]=f}}else{f=[i];fragmentByStart[f.start=start]=fragmentByEnd[f.end=end]=f}});function ends(i){var arc=topology.arcs[i<0?~i:i],p0=arc[0],p1;if(topology.transform)p1=[0,0],arc.forEach(function(dp){p1[0]+=dp[0],p1[1]+=dp[1]});else p1=arc[arc.length-1];return i<0?[p1,p0]:[p0,p1]}function flush(fragmentByEnd,fragmentByStart){for(var k in fragmentByEnd){var f=fragmentByEnd[k];delete fragmentByStart[f.start];delete f.start;delete f.end;f.forEach(function(i){stitchedArcs[i<0?~i:i]=1});fragments.push(f)}}flush(fragmentByEnd,fragmentByStart);flush(fragmentByStart,fragmentByEnd);arcs.forEach(function(i){if(!stitchedArcs[i<0?~i:i])fragments.push([i])});return fragments}function meshArcs(topology,o,filter){var arcs=[];if(arguments.length>1){var geomsByArc=[],geom;function arc(i){var j=i<0?~i:i;(geomsByArc[j]||(geomsByArc[j]=[])).push({i:i,g:geom})}function line(arcs){arcs.forEach(arc)}function polygon(arcs){arcs.forEach(line)}function geometry(o){if(o.type==="GeometryCollection")o.geometries.forEach(geometry);else if(o.type in geometryType)geom=o,geometryType[o.type](o.arcs)}var geometryType={LineString:line,MultiLineString:polygon,Polygon:polygon,MultiPolygon:function(arcs){arcs.forEach(polygon)}};geometry(o);geomsByArc.forEach(arguments.length<3?function(geoms){arcs.push(geoms[0].i)}:function(geoms){if(filter(geoms[0].g,geoms[geoms.length-1].g))arcs.push(geoms[0].i)})}else{for(var i=0,n=topology.arcs.length;i<n;++i)arcs.push(i)}return{type:"MultiLineString",arcs:stitchArcs(topology,arcs)}}function mergeArcs(topology,objects){var polygonsByArc={},polygons=[],components=[];objects.forEach(function(o){if(o.type==="Polygon")register(o.arcs);else if(o.type==="MultiPolygon")o.arcs.forEach(register)});function register(polygon){polygon.forEach(function(ring){ring.forEach(function(arc){(polygonsByArc[arc=arc<0?~arc:arc]||(polygonsByArc[arc]=[])).push(polygon)})});polygons.push(polygon)}function exterior(ring){return cartesianRingArea(object(topology,{type:"Polygon",arcs:[ring]}).coordinates[0])>0}polygons.forEach(function(polygon){if(!polygon._){var component=[],neighbors=[polygon];polygon._=1;components.push(component);while(polygon=neighbors.pop()){component.push(polygon);polygon.forEach(function(ring){ring.forEach(function(arc){polygonsByArc[arc<0?~arc:arc].forEach(function(polygon){if(!polygon._){polygon._=1;neighbors.push(polygon)}})})})}}});polygons.forEach(function(polygon){delete polygon._});return{type:"MultiPolygon",arcs:components.map(function(polygons){var arcs=[];polygons.forEach(function(polygon){polygon.forEach(function(ring){ring.forEach(function(arc){if(polygonsByArc[arc<0?~arc:arc].length<2){arcs.push(arc)}})})});arcs=stitchArcs(topology,arcs);if((n=arcs.length)>1){var sgn=exterior(polygons[0][0]);for(var i=0,t;i<n;++i){if(sgn===exterior(arcs[i])){t=arcs[0],arcs[0]=arcs[i],arcs[i]=t;break}}}return arcs})}}function featureOrCollection(topology,o){return o.type==="GeometryCollection"?{type:"FeatureCollection",features:o.geometries.map(function(o){return feature(topology,o)})}:feature(topology,o)}function feature(topology,o){var f={type:"Feature",id:o.id,properties:o.properties||{},geometry:object(topology,o)};if(o.id==null)delete f.id;return f}function object(topology,o){var absolute=transformAbsolute(topology.transform),arcs=topology.arcs;function arc(i,points){if(points.length)points.pop();for(var a=arcs[i<0?~i:i],k=0,n=a.length,p;k<n;++k){points.push(p=a[k].slice());absolute(p,k)}if(i<0)reverse(points,n)}function point(p){p=p.slice();absolute(p,0);return p}function line(arcs){var points=[];for(var i=0,n=arcs.length;i<n;++i)arc(arcs[i],points);if(points.length<2)points.push(points[0].slice());return points}function ring(arcs){var points=line(arcs);while(points.length<4)points.push(points[0].slice());return points}function polygon(arcs){return arcs.map(ring)}function geometry(o){var t=o.type;return t==="GeometryCollection"?{type:t,geometries:o.geometries.map(geometry)}:t in geometryType?{type:t,coordinates:geometryType[t](o)}:null}var geometryType={Point:function(o){return point(o.coordinates)},MultiPoint:function(o){return o.coordinates.map(point)},LineString:function(o){return line(o.arcs)},MultiLineString:function(o){return o.arcs.map(line)},Polygon:function(o){return polygon(o.arcs)},MultiPolygon:function(o){return o.arcs.map(polygon)}};return geometry(o)}function reverse(array,n){var t,j=array.length,i=j-n;while(i<--j)t=array[i],array[i++]=array[j],array[j]=t}function bisect(a,x){var lo=0,hi=a.length;while(lo<hi){var mid=lo+hi>>>1;if(a[mid]<x)lo=mid+1;else hi=mid}return lo}function neighbors(objects){var indexesByArc={},neighbors=objects.map(function(){return[]});function line(arcs,i){arcs.forEach(function(a){if(a<0)a=~a;var o=indexesByArc[a];if(o)o.push(i);else indexesByArc[a]=[i]})}function polygon(arcs,i){arcs.forEach(function(arc){line(arc,i)})}function geometry(o,i){if(o.type==="GeometryCollection")o.geometries.forEach(function(o){geometry(o,i)});else if(o.type in geometryType)geometryType[o.type](o.arcs,i)}var geometryType={LineString:line,MultiLineString:polygon,Polygon:polygon,MultiPolygon:function(arcs,i){arcs.forEach(function(arc){polygon(arc,i)})}};objects.forEach(geometry);for(var i in indexesByArc){for(var indexes=indexesByArc[i],m=indexes.length,j=0;j<m;++j){for(var k=j+1;k<m;++k){var ij=indexes[j],ik=indexes[k],n;if((n=neighbors[ij])[i=bisect(n,ik)]!==ik)n.splice(i,0,ik);if((n=neighbors[ik])[i=bisect(n,ij)]!==ij)n.splice(i,0,ij)}}}return neighbors}function presimplify(topology,triangleArea){var absolute=transformAbsolute(topology.transform),relative=transformRelative(topology.transform),heap=minAreaHeap();if(!triangleArea)triangleArea=cartesianTriangleArea;topology.arcs.forEach(function(arc){var triangles=[],maxArea=0,triangle;for(var i=0,n=arc.length,p;i<n;++i){p=arc[i];arc[i]=[p[0],p[1],Infinity,p[0],p[1]];absolute(arc[i],i)}for(var i=1,n=arc.length-1;i<n;++i){triangle=arc.slice(i-1,i+2);triangle[1][2]=triangleArea(triangle);triangles.push(triangle);heap.push(triangle)}for(var i=0,n=triangles.length;i<n;++i){triangle=triangles[i];triangle.previous=triangles[i-1];triangle.next=triangles[i+1]}while(triangle=heap.pop()){var previous=triangle.previous,next=triangle.next;if(triangle[1][2]<maxArea)triangle[1][2]=maxArea;else maxArea=triangle[1][2];if(previous){previous.next=next;previous[2]=triangle[2];update(previous)}if(next){next.previous=previous;next[0]=triangle[0];update(next)}}for(var i=0,n=arc.length,p;i<n;++i){p=arc[i];arc[i]=[p[3],p[4],p[2]]}});function update(triangle){heap.remove(triangle);triangle[1][2]=triangleArea(triangle);heap.push(triangle)}return topology}function cartesianRingArea(ring){var i=-1,n=ring.length,a,b=ring[n-1],area=0;while(++i<n){a=b;b=ring[i];area+=a[0]*b[1]-a[1]*b[0]}return area*.5}function cartesianTriangleArea(triangle){var a=triangle[0],b=triangle[1],c=triangle[2];return Math.abs((a[0]-c[0])*(b[1]-a[1])-(a[0]-b[0])*(c[1]-a[1]))}function compareArea(a,b){return a[1][2]-b[1][2]}function minAreaHeap(){var heap={},array=[],size=0;heap.push=function(object){up(array[object._=size]=object,size++);return size};heap.pop=function(){if(size<=0)return;var removed=array[0],object;if(--size>0)object=array[size],down(array[object._=0]=object,0);return removed};heap.remove=function(removed){var i=removed._,object;if(array[i]!==removed)return;if(i!==--size)object=array[size],(compareArea(object,removed)<0?up:down)(array[object._=i]=object,i);return i};function up(object,i){while(i>0){var j=(i+1>>1)-1,parent=array[j];if(compareArea(object,parent)>=0)break;array[parent._=i]=parent;array[object._=i=j]=object}}function down(object,i){while(true){var r=i+1<<1,l=r-1,j=i,child=array[j];if(l<size&&compareArea(array[l],child)<0)child=array[j=l];if(r<size&&compareArea(array[r],child)<0)child=array[j=r];if(j===i)break;array[child._=i]=child;array[object._=i=j]=object}}return heap}function transformAbsolute(transform){if(!transform)return noop;var x0,y0,kx=transform.scale[0],ky=transform.scale[1],dx=transform.translate[0],dy=transform.translate[1];return function(point,i){if(!i)x0=y0=0;point[0]=(x0+=point[0])*kx+dx;point[1]=(y0+=point[1])*ky+dy}}function transformRelative(transform){if(!transform)return noop;var x0,y0,kx=transform.scale[0],ky=transform.scale[1],dx=transform.translate[0],dy=transform.translate[1];return function(point,i){if(!i)x0=y0=0;var x1=(point[0]-dx)/kx|0,y1=(point[1]-dy)/ky|0;point[0]=x1-x0;point[1]=y1-y0;x0=x1;y0=y1}}function noop(){}if(typeof define==="function"&&define.amd)define(topojson);else if(typeof module==="object"&&module.exports)module.exports=topojson;else this.topojson=topojson}();
  6. martgnz revised this gist Apr 5, 2016. 1 changed file with 0 additions and 2 deletions.
    2 changes: 0 additions & 2 deletions index.html
    Original file line number Diff line number Diff line change
    @@ -39,11 +39,9 @@
    parameters.context.textAlign = "center"
    parameters.context.font = "bold " + (12 / parameters.scale) + "px " + "sans-serif"
    parameters.context.fillStyle = "rgb(70,70,70)"
    parameters.context.shadowColor = "transparent";

    parameters.context.arc(projectedPoint[0], projectedPoint[1] + 4 / parameters.scale, 2 / parameters.scale, 0, 2 * Math.PI, true)
    parameters.context.fillText(d.name, projectedPoint[0], projectedPoint[1])
    parameters.context.shadowColor = "transparent";
    parameters.context.fill()
    })
    }
  7. martgnz revised this gist Apr 5, 2016. 1 changed file with 33 additions and 38 deletions.
    71 changes: 33 additions & 38 deletions index.html
    Original file line number Diff line number Diff line change
    @@ -1,62 +1,57 @@
    <!DOCTYPE html>
    <meta charset="utf-8" />
    <body>
    <div class="js-map"></div>

    <script src="d3.js"></script>
    <script src="topojson.js"></script>
    <script src="rbush.js"></script>
    <script src="spam.js"></script>

    <script type='text/javascript'>
    d3.tsv("antarctic-bases.tsv", function(error, antarctica) {
    antarctica.forEach(function(d) {
    d.lat = +d.lat
    d.lng = +d.lng
    })
    bases = antarctica
    })

    d3.json("antarctica.json", function(error, d) {
    topojson.presimplify(d)
    d3.json("antarctica.json", function(error, d) {
    topojson.presimplify(d)

    var map = new StaticCanvasMap({
    element: ".js-map",
    width: 960,
    height: 900,
    projection: d3.geo.stereographic()
    .scale(1000)
    .translate([960 / 2, -650])
    .clipAngle(180 - 1e-4)
    .precision(.1),
    data: [
    {
    features: topojson.feature(d, d.objects["antarctica"]),
    paintfeature: function(parameters, d) {
    parameters.context.lineWidth = 0.5 / parameters.scale
    parameters.context.strokeStyle = "rgb(30,30,30)"
    parameters.context.stroke()
    },
    postpaint: function(parameters, d) {
    for (var i in bases) {
    var base = bases[i]
    var projectedPoint = parameters.map.settings().projection([base.lng, base.lat])
    var map = new StaticCanvasMap({
    element: "body",
    width: 960,
    height: 900,
    projection: d3.geo.stereographic()
    .scale(1000)
    .translate([960 / 2, -650])
    .clipAngle(180 - 1e-4)
    .precision(.1),
    data: [
    {
    features: topojson.feature(d, d.objects["antarctica"]),
    paintfeature: function(parameters, d) {
    parameters.context.lineWidth = 0.5 / parameters.scale
    parameters.context.strokeStyle = "rgb(170,170,170)"
    parameters.context.stroke()
    },
    postpaint: function(parameters, d) {
    antarctica.filter(function(d) {
    return d.countries === "Russia"
    })
    .forEach(function(d) {
    var projectedPoint = parameters.map.settings().projection([+d.lng, +d.lat])

    if (base.countries === "Russia") {
    parameters.context.beginPath()
    parameters.context.textAlign = "center"
    parameters.context.font = "bold " + (12 / parameters.scale) + "px " + "sans-serif"
    parameters.context.fillStyle = "rgb(70,70,70)"
    parameters.context.shadowColor = "transparent";

    parameters.context.arc(projectedPoint[0], projectedPoint[1] + 4 / parameters.scale, 2 / parameters.scale, 0, 2 * Math.PI, true)
    parameters.context.fillText(base.name, projectedPoint[0], projectedPoint[1])
    parameters.context.fillText(d.name, projectedPoint[0], projectedPoint[1])
    parameters.context.shadowColor = "transparent";
    parameters.context.fill()
    }
    })
    }
    }
    }
    ]
    ]
    })
    map.init()
    })
    map.init()
    })
    </script>
    </body>
  8. martgnz revised this gist Apr 3, 2016. 3 changed files with 20 additions and 19 deletions.
    2 changes: 1 addition & 1 deletion README.md
    Original file line number Diff line number Diff line change
    @@ -1,3 +1,3 @@
    A static map of Antarctica made with Spam.js.

    Custom labels.
    Data from [Wikipedia](https://en.wikipedia.org/wiki/Research_stations_in_Antarctica). The labels are parsed with `d3.tsv` from an external file.
    37 changes: 19 additions & 18 deletions index.html
    Original file line number Diff line number Diff line change
    @@ -9,6 +9,14 @@
    <script src="spam.js"></script>

    <script type='text/javascript'>
    d3.tsv("antarctic-bases.tsv", function(error, antarctica) {
    antarctica.forEach(function(d) {
    d.lat = +d.lat
    d.lng = +d.lng
    })
    bases = antarctica
    })

    d3.json("antarctica.json", function(error, d) {
    topojson.presimplify(d)

    @@ -30,27 +38,20 @@
    parameters.context.stroke()
    },
    postpaint: function(parameters, d) {
    d3.tsv("antarctic-bases.tsv", function(error, antarctica) {
    antarctica.forEach(function(d) {
    d.lat = +d.lat
    d.lng = +d.lng
    })
    for (var i in bases) {
    var base = bases[i]
    var projectedPoint = parameters.map.settings().projection([base.lng, base.lat])

    for (var i in antarctica) {
    var base = antarctica[i]
    var projectedPoint = parameters.map.settings().projection([base.lng, base.lat])
    if (base.countries === "Russia") {
    parameters.context.beginPath()
    parameters.context.textAlign = "center"
    parameters.context.font = "bold " + (12 / parameters.scale) + "px " + "sans-serif"

    if (base.countries === "Russia") {
    parameters.context.beginPath()
    parameters.context.textAlign = "right"
    parameters.context.font = "bold" + " " + (11 / parameters.scale) + "px " + "Helvetica Neue, sans-serif"

    parameters.context.arc(projectedPoint[0], projectedPoint[1] + 4 / parameters.scale, 2 / parameters.scale, 0, 2 * Math.PI, true)
    parameters.context.fillText(base.name, projectedPoint[0], projectedPoint[1])
    parameters.context.fill()
    }
    parameters.context.arc(projectedPoint[0], projectedPoint[1] + 4 / parameters.scale, 2 / parameters.scale, 0, 2 * Math.PI, true)
    parameters.context.fillText(base.name, projectedPoint[0], projectedPoint[1])
    parameters.context.fill()
    }
    })
    }
    }
    }
    ]
    Binary file added thumbnail.png
    Loading
    Sorry, something went wrong. Reload?
    Sorry, we cannot display this file.
    Sorry, this file is invalid so it cannot be displayed.
  9. martgnz revised this gist Apr 3, 2016. 1 changed file with 1 addition and 1 deletion.
    2 changes: 1 addition & 1 deletion README.md
    Original file line number Diff line number Diff line change
    @@ -1,3 +1,3 @@
    Chile map made with Spam.js.
    A static map of Antarctica made with Spam.js.

    Custom labels.
  10. martgnz revised this gist Apr 3, 2016. 1 changed file with 1 addition and 0 deletions.
    1 change: 1 addition & 0 deletions .block
    Original file line number Diff line number Diff line change
    @@ -1,2 +1,3 @@
    license: mit
    border: none
    height: 900
  11. martgnz created this gist Apr 3, 2016.
    2 changes: 2 additions & 0 deletions .block
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,2 @@
    license: mit
    border: none
    3 changes: 3 additions & 0 deletions README.md
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,3 @@
    Chile map made with Spam.js.

    Custom labels.
    77 changes: 77 additions & 0 deletions antarctic-bases.tsv
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,77 @@
    name open countries established operator situation lat lng
    Aboa Summer Finland 1989 Finnish Antarctic Research Program Queen Maud Land -73.050 -13.417
    Almirante Brown Antarctic Base Summer Argentina 1951 Argentine Antarctic Institute Antarctic Peninsula -64.895306 -62.870500
    Amundsen–Scott South Pole Station Permanent United States 1957 United States Antarctic Program Geographical South Pole -90 0
    Artigas Base Permanent Uruguay 1984 Uruguayan Antarctic Institute King George Island -62.184278 -58.903306
    Asuka Station Summer Japan 1985 National Institute of Polar Research unmanned observation Queen Maud Land -71.52611 24.13806
    Belgrano II Permanent Argentina 1979 Argentine Antarctic Institute Coats Land -77.874389 -34.620806
    Bellingshausen Station Permanent Russia 1968 Russian Antarctic Expedition King George Island -62.19639 -58.96083
    Bernardo O'Higgins Station Permanent Chile-Germany 1948-1991 Chilean Army Logistics German Aerospace Center Antarctic Peninsula -63.32083 -57.898944
    Bharati Permanent India 2012 Indian Antarctic Program Larsemann Hills -69.40778 76.18722
    Byrd Station Summer United States 1957 United States Antarctic Program Marie Byrd Land -80.01667 -119.53333
    Cámara Base Summer Argentina 1953 Argentine Antarctic Institute Half Moon Island -62.59472 -59.91889
    Captain Arturo Prat Base Permanent Chile 1947 Chilean Navy Greenwich Island -62.47917 -59.66417
    Carlini Base (Ex-Jubany) Permanent Argentina 1953 Argentine Antarctic Institute King George Island -62.237972 -58.666722
    Casey Station Permanent Australia 1957 Australian Antarctic Division Vincennes Bay -66.282111 110.525528
    Comandante Ferraz Antarctic Station Permanent Brazil 1984 Brazilian Antarctic Program King George Island -62.08333 -58.391167
    Concordia Station Permanent Italy-France 2005 Concordia Station is a joint French-Italian research facility managed by PNRA (National Antarctic Research Program of Italy) and IPEV (Institut Polaire Français Paul Émile Victor) research topics: human biologygeomagnetic observations geodesy glaciology meteorological observations astronomy seismology and environmental monitoring Dome C Antarctic Plateau -75.10000 123.33333
    Dakshin Gangotri Permanent India 1983 Indian Antarctic Program Dakshin Gangotri Glacier near Schirmacher Oasis -70.09361 12.00000
    Davis Station Permanent Australia 1957 Australian Antarctic Division Princess Elizabeth Land -68.576472 77.969222
    Decepción Base Summer Argentina 1948 Argentine Antarctic Institute Deception Island -62.97528 -60.69778
    Dome Fuji Station Summer Japan 1995 National Institute of Polar Research Queen Maud Land -77.31694 39.70333
    Druzhnaya 4 Summer Russia 1987-1991 (re-opening in 1995) Russian Antarctic Expedition Princess Elizabeth Land -69.733583 73.700167
    Dumont d'Urville Station Permanent France 1956 IPEV (Institut Polaire Français Paul Émile Victor) Adélie Land -66.663139 140.001472
    Base Presidente Eduardo Frei Montalva and Villa Las Estrellas Permanent Chile 1969 Chilean Air Force King George Island -62.1950 -58.9783
    Esperanza Base Permanent Argentina 1953 Argentine Antarctic Institute Hope Bay -63.397306 -56.997028
    Gabriel de Castilla Station Summer Spain 1989 CSIC Marine biology Deception Island -62.97694 -60.67556
    Gonzalez Videla Station Summer Chile 1951 Chilean Air Force Paradise Bay Water Boat Point -64.82333 -62.85806
    Great Wall Station Permanent China 1985 Polar Research Institute of China King George Island -62.21722 -58.961528
    Halley Research Station Permanent United Kingdom 1956 British Antarctic Survey Brunt Ice Shelf -75.58333 -26.56667
    Henryk Arctowski Polish Antarctic Station Permanent Poland 1977 Polish Academy of Sciences King George Island -62.1500389 -58.467250
    Jang Bogo Station Permanent South Korea 2014 Korea Antarctic Research Program Terra Nova Bay -74.61667 164.20139
    Jinnah Antarctic Station Summer Pakistan 1991 Pakistan Antarctic Programme Sør Rondane Mountains Queen Maud Land -70.400 25.750
    Juan Carlos I Station Summer Spain 1988 CSIC Laboratory investigation and meteorogical station. South Bay Livingston Island -62.662750 -60.390361
    King Sejong Station Permanent South Korea 1988 Korea Antarctic Research Program King George Island -62.223111 -58.787056
    Kohnen Station Summer Germany 2001 Alfred Wegener Institute Queen Maud Land -75.000 0.067
    Kunlun Station Summer China 2009 Polar Research Institute of China Dome A -80.41694 77.11611
    Law-Racovi Station Permanent Romania 1986 Romanian Polar Research Institute Larsemann Hills Princess Elizabeth Land -69.388583 76.3807639
    Leningradskaya Station Summer Russia 1971-1991 (re-opening in 2007-2008) Russian Antarctic Expedition Oates Coast Victoria Land -69.50000 159.38333
    Machu Picchu Research Station Summer Peru 1989 Peruvian Antarctic Institute (INANPE) Admiralty Bay King George Island -62.091639 -58.470944
    Maitri Station Permanent India 1989 Indian Antarctic Program Schirmacher Oasis -70.766028 11.732278
    Maldonado Base Summer Ecuador 1990 Ecuadorian Antarctic Institute Greenwich Island -62.449056 -59.74139
    Marambio Base Permanent Argentina 1969 Argentine Antarctic Institute Seymour-Marambio Island -64.240861 -56.624083
    Mario Zucchelli Station Permanent Italy 1986 National Antarctic Research Program (PNRA) with the collaboration of ENEA and CNR research topics: offshore marine biology terrestrial biology oceanography geomagnetic observations geodesy onshore geology glaciology meteorological observations ionospheric/auroral observations cosmic ray observations seismology and environmental monitoring Terra Nova Bay Ross Sea -74.694417 164.112917
    Matienzo Base Summer Argentina 1961 Argentine Antarctic Institute Graham Land -64.97583 -60.07167
    Mawson Station Permanent Australia 1954 Australian Antarctic Division Mac Robertson Land -67.602806 62.873000
    McMurdo Station Permanent United States 1956 United States Antarctic Program Ross Island -77.845389 166.669778
    Melchior Base Summer Argentina 1947 Argentine Antarctic Institute Melchior Islands -64.32556 -62.97611
    Mendel Polar Station Summer Czech Republic 2006 Masaryk University biological geological and climate research James Ross Island -63.801806 -57.885528
    Mirny Station Permanent Russia 1956 Russian Antarctic Expedition glaciology seismology meteorology polar lights cosmic radiation and marine biology Davis Sea -66.552889 93.009667
    Mizuho Station Summer Japan 1970 National Institute of Polar Research Transshipment station - -70.69806 44.33167
    Molodyozhnaya Station Summer Russia-Belarus 1962-1990 (re-opening in 2007-2008) Russian Antarctic Expedition Meteorology - -67.665833 45.842556
    Neumayer-Station III Permanent Germany 2009 Alfred Wegener Institute Atka Bay -70.66889 -8.2672083
    Novolazarevskaya Station Permanent Russia 1961 Russian Antarctic Expedition Queen Maud Land -70.822528 11.644472
    Orcadas Base Permanent Argentina 1904 Argentine Antarctic Institute Argentine Navy Laurie Island South Orkney Islands -60.737639 -44.73944
    Palmer Station Permanent United States 1968 United States Antarctic Program Science labs a dock and a helicopter pad. Anvers Island -64.774194 -64.05306
    Petrel Base Summer Argentina 1952 Argentine Antarctic Institute Dundee Island -63.47833 -56.23194
    Primavera Base Summer Argentina 1977 Argentine Antarctic Institute Graham Land -64.15583 -60.95500
    Princess Elisabeth Base Permanent Belgium 2007 Belgium Polar Secretariat Energy-passive research station. Queen Maud Land -71.57000 23.20000
    Professor Julio Escudero Base Permanent Chile 1994 Chilean Antarctic Institute King George Island -62.201167 -58.962583
    Progress Station Summer Russia 1988 Russian Antarctic Expedition Prydz Bay -69.380056 76.388639
    Rothera Research Station Permanent United Kingdom 1975 British Antarctic Survey Adelaide Island -67.568972 -68.124750
    Russkaya Station Summer Russia 1980-1990 (re-opening in 2007-2008) Russian Antarctic Expedition Marie Byrd Land -74.76667 -136.86667
    San Martín Base Permanent Argentina 1951 Argentine Antarctic Institute Barry Island -68.130250 -67.102000
    SANAE IV (South African National Antarctic Expedition) Permanent South Africa 1962 (SANAE I) South African National Antarctic Programme Vesleskarvet in Queen Maud Land -71.672750 -2.840250
    St. Kliment Ohridski Base Permanent Bulgaria 1988 Bulgarian Antarctic Institute Biological research laboratorial and meteorological measurements. First Eastern Orthodox chapel St. Ivan Rilski Emona Anchorage Livingston Island -62.64139 -60.36472
    Scott Base Permanent New Zealand 1957 Antarctica New Zealand Antarctic physical environments Southern Ocean and Antarctic ecosystems. Ross Island -77.849583 166.768306
    Showa Station Permanent Japan 1957 National Institute of Polar Research East Ongul Island -69.004333 39.580250
    Signy Research Station Summer (Permanent 1947-1995) United Kingdom 1947 British Antarctic Survey Signy Island South Orkney Islands -60.717 -45.600
    Svea Research Station Summer Sweden 1988 Swedish Polar Research Secretariat Queen Maud Land -74.57611 -11.22528
    Taishan Station Summer China 2014 Polar Research Institute of China Princess Elizabeth Land -73.850 76.967
    Tor Station Summer Norway 1993 Norwegian Polar Institute Queen Maud Land -71.88889 5.15833
    Troll Station Permanent Norway 1990 Norwegian Polar Institute Queen Maud Land -72.012083 2.53222
    WAIS Divide Camp Summer United States 2005 United States Antarctic Program Collect a deep ice core West Antarctic Ice Sheet -79.467 -112.067
    Wasa Research Station Summer Sweden 1989 Swedish Polar Research Secretariat Queen Maud Land -73.050 -13.417
    Vernadsky Research Base Permanent Ukraine 1994 National Antarctic Scientific Center Galindez Island -65.245722 -64.25722
    Vostok Station Permanent Russia 1957 Russian Antarctic Expedition Antarctic Ice Sheet -78.464389 106.83722
    Zhongshan (Sun Yat-Sen) Station Permanent China 1989 Polar Research Institute of China Larsemann Hills in Prydz Bay -69.37333 76.37000
    1 change: 1 addition & 0 deletions antarctica.json
    1 addition, 0 deletions not shown because the diff is too large. Please use a local Git client to view these changes.
    9,563 changes: 9,563 additions & 0 deletions d3.js
    9,563 additions, 0 deletions not shown because the diff is too large. Please use a local Git client to view these changes.
    62 changes: 62 additions & 0 deletions index.html
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,62 @@
    <!DOCTYPE html>
    <meta charset="utf-8" />
    <body>
    <div class="js-map"></div>

    <script src="d3.js"></script>
    <script src="topojson.js"></script>
    <script src="rbush.js"></script>
    <script src="spam.js"></script>

    <script type='text/javascript'>
    d3.json("antarctica.json", function(error, d) {
    topojson.presimplify(d)

    var map = new StaticCanvasMap({
    element: ".js-map",
    width: 960,
    height: 900,
    projection: d3.geo.stereographic()
    .scale(1000)
    .translate([960 / 2, -650])
    .clipAngle(180 - 1e-4)
    .precision(.1),
    data: [
    {
    features: topojson.feature(d, d.objects["antarctica"]),
    paintfeature: function(parameters, d) {
    parameters.context.lineWidth = 0.5 / parameters.scale
    parameters.context.strokeStyle = "rgb(30,30,30)"
    parameters.context.stroke()
    },
    postpaint: function(parameters, d) {
    d3.tsv("antarctic-bases.tsv", function(error, antarctica) {
    antarctica.forEach(function(d) {
    d.lat = +d.lat
    d.lng = +d.lng
    })

    for (var i in antarctica) {
    var base = antarctica[i]
    var projectedPoint = parameters.map.settings().projection([base.lng, base.lat])

    if (base.countries === "Russia") {
    parameters.context.beginPath()
    parameters.context.textAlign = "right"
    parameters.context.font = "bold" + " " + (11 / parameters.scale) + "px " + "Helvetica Neue, sans-serif"

    parameters.context.arc(projectedPoint[0], projectedPoint[1] + 4 / parameters.scale, 2 / parameters.scale, 0, 2 * Math.PI, true)
    parameters.context.fillText(base.name, projectedPoint[0], projectedPoint[1])
    parameters.context.fill()
    }
    }
    })
    }
    }
    ]
    })
    map.init()
    })
    </script>
    </body>
    </html>
    621 changes: 621 additions & 0 deletions rbush.js
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,621 @@
    /*
    (c) 2015, Vladimir Agafonkin
    RBush, a JavaScript library for high-performance 2D spatial indexing of points and rectangles.
    https://github.com/mourner/rbush
    */

    (function () {
    'use strict';

    function rbush(maxEntries, format) {

    // jshint newcap: false, validthis: true
    if (!(this instanceof rbush)) return new rbush(maxEntries, format);

    // max entries in a node is 9 by default; min node fill is 40% for best performance
    this._maxEntries = Math.max(4, maxEntries || 9);
    this._minEntries = Math.max(2, Math.ceil(this._maxEntries * 0.4));

    if (format) {
    this._initFormat(format);
    }

    this.clear();
    }

    rbush.prototype = {

    all: function () {
    return this._all(this.data, []);
    },

    search: function (bbox) {

    var node = this.data,
    result = [],
    toBBox = this.toBBox;

    if (!intersects(bbox, node.bbox)) return result;

    var nodesToSearch = [],
    i, len, child, childBBox;

    while (node) {
    for (i = 0, len = node.children.length; i < len; i++) {

    child = node.children[i];
    childBBox = node.leaf ? toBBox(child) : child.bbox;

    if (intersects(bbox, childBBox)) {
    if (node.leaf) result.push(child);
    else if (contains(bbox, childBBox)) this._all(child, result);
    else nodesToSearch.push(child);
    }
    }
    node = nodesToSearch.pop();
    }

    return result;
    },

    collides: function (bbox) {

    var node = this.data,
    toBBox = this.toBBox;

    if (!intersects(bbox, node.bbox)) return false;

    var nodesToSearch = [],
    i, len, child, childBBox;

    while (node) {
    for (i = 0, len = node.children.length; i < len; i++) {

    child = node.children[i];
    childBBox = node.leaf ? toBBox(child) : child.bbox;

    if (intersects(bbox, childBBox)) {
    if (node.leaf || contains(bbox, childBBox)) return true;
    nodesToSearch.push(child);
    }
    }
    node = nodesToSearch.pop();
    }

    return false;
    },

    load: function (data) {
    if (!(data && data.length)) return this;

    if (data.length < this._minEntries) {
    for (var i = 0, len = data.length; i < len; i++) {
    this.insert(data[i]);
    }
    return this;
    }

    // recursively build the tree with the given data from stratch using OMT algorithm
    var node = this._build(data.slice(), 0, data.length - 1, 0);

    if (!this.data.children.length) {
    // save as is if tree is empty
    this.data = node;

    } else if (this.data.height === node.height) {
    // split root if trees have the same height
    this._splitRoot(this.data, node);

    } else {
    if (this.data.height < node.height) {
    // swap trees if inserted one is bigger
    var tmpNode = this.data;
    this.data = node;
    node = tmpNode;
    }

    // insert the small tree into the large tree at appropriate level
    this._insert(node, this.data.height - node.height - 1, true);
    }

    return this;
    },

    insert: function (item) {
    if (item) this._insert(item, this.data.height - 1);
    return this;
    },

    clear: function () {
    this.data = {
    children: [],
    height: 1,
    bbox: empty(),
    leaf: true
    };
    return this;
    },

    remove: function (item) {
    if (!item) return this;

    var node = this.data,
    bbox = this.toBBox(item),
    path = [],
    indexes = [],
    i, parent, index, goingUp;

    // depth-first iterative tree traversal
    while (node || path.length) {

    if (!node) { // go up
    node = path.pop();
    parent = path[path.length - 1];
    i = indexes.pop();
    goingUp = true;
    }

    if (node.leaf) { // check current node
    index = node.children.indexOf(item);

    if (index !== -1) {
    // item found, remove the item and condense tree upwards
    node.children.splice(index, 1);
    path.push(node);
    this._condense(path);
    return this;
    }
    }

    if (!goingUp && !node.leaf && contains(node.bbox, bbox)) { // go down
    path.push(node);
    indexes.push(i);
    i = 0;
    parent = node;
    node = node.children[0];

    } else if (parent) { // go right
    i++;
    node = parent.children[i];
    goingUp = false;

    } else node = null; // nothing found
    }

    return this;
    },

    toBBox: function (item) { return item; },

    compareMinX: function (a, b) { return a[0] - b[0]; },
    compareMinY: function (a, b) { return a[1] - b[1]; },

    toJSON: function () { return this.data; },

    fromJSON: function (data) {
    this.data = data;
    return this;
    },

    _all: function (node, result) {
    var nodesToSearch = [];
    while (node) {
    if (node.leaf) result.push.apply(result, node.children);
    else nodesToSearch.push.apply(nodesToSearch, node.children);

    node = nodesToSearch.pop();
    }
    return result;
    },

    _build: function (items, left, right, height) {

    var N = right - left + 1,
    M = this._maxEntries,
    node;

    if (N <= M) {
    // reached leaf level; return leaf
    node = {
    children: items.slice(left, right + 1),
    height: 1,
    bbox: null,
    leaf: true
    };
    calcBBox(node, this.toBBox);
    return node;
    }

    if (!height) {
    // target height of the bulk-loaded tree
    height = Math.ceil(Math.log(N) / Math.log(M));

    // target number of root entries to maximize storage utilization
    M = Math.ceil(N / Math.pow(M, height - 1));
    }

    node = {
    children: [],
    height: height,
    bbox: null,
    leaf: false
    };

    // split the items into M mostly square tiles

    var N2 = Math.ceil(N / M),
    N1 = N2 * Math.ceil(Math.sqrt(M)),
    i, j, right2, right3;

    multiSelect(items, left, right, N1, this.compareMinX);

    for (i = left; i <= right; i += N1) {

    right2 = Math.min(i + N1 - 1, right);

    multiSelect(items, i, right2, N2, this.compareMinY);

    for (j = i; j <= right2; j += N2) {

    right3 = Math.min(j + N2 - 1, right2);

    // pack each entry recursively
    node.children.push(this._build(items, j, right3, height - 1));
    }
    }

    calcBBox(node, this.toBBox);

    return node;
    },

    _chooseSubtree: function (bbox, node, level, path) {

    var i, len, child, targetNode, area, enlargement, minArea, minEnlargement;

    while (true) {
    path.push(node);

    if (node.leaf || path.length - 1 === level) break;

    minArea = minEnlargement = Infinity;

    for (i = 0, len = node.children.length; i < len; i++) {
    child = node.children[i];
    area = bboxArea(child.bbox);
    enlargement = enlargedArea(bbox, child.bbox) - area;

    // choose entry with the least area enlargement
    if (enlargement < minEnlargement) {
    minEnlargement = enlargement;
    minArea = area < minArea ? area : minArea;
    targetNode = child;

    } else if (enlargement === minEnlargement) {
    // otherwise choose one with the smallest area
    if (area < minArea) {
    minArea = area;
    targetNode = child;
    }
    }
    }

    node = targetNode;
    }

    return node;
    },

    _insert: function (item, level, isNode) {

    var toBBox = this.toBBox,
    bbox = isNode ? item.bbox : toBBox(item),
    insertPath = [];

    // find the best node for accommodating the item, saving all nodes along the path too
    var node = this._chooseSubtree(bbox, this.data, level, insertPath);

    // put the item into the node
    node.children.push(item);
    extend(node.bbox, bbox);

    // split on node overflow; propagate upwards if necessary
    while (level >= 0) {
    if (insertPath[level].children.length > this._maxEntries) {
    this._split(insertPath, level);
    level--;
    } else break;
    }

    // adjust bboxes along the insertion path
    this._adjustParentBBoxes(bbox, insertPath, level);
    },

    // split overflowed node into two
    _split: function (insertPath, level) {

    var node = insertPath[level],
    M = node.children.length,
    m = this._minEntries;

    this._chooseSplitAxis(node, m, M);

    var splitIndex = this._chooseSplitIndex(node, m, M);

    var newNode = {
    children: node.children.splice(splitIndex, node.children.length - splitIndex),
    height: node.height,
    bbox: null,
    leaf: false
    };

    if (node.leaf) newNode.leaf = true;

    calcBBox(node, this.toBBox);
    calcBBox(newNode, this.toBBox);

    if (level) insertPath[level - 1].children.push(newNode);
    else this._splitRoot(node, newNode);
    },

    _splitRoot: function (node, newNode) {
    // split root node
    this.data = {
    children: [node, newNode],
    height: node.height + 1,
    bbox: null,
    leaf: false
    };
    calcBBox(this.data, this.toBBox);
    },

    _chooseSplitIndex: function (node, m, M) {

    var i, bbox1, bbox2, overlap, area, minOverlap, minArea, index;

    minOverlap = minArea = Infinity;

    for (i = m; i <= M - m; i++) {
    bbox1 = distBBox(node, 0, i, this.toBBox);
    bbox2 = distBBox(node, i, M, this.toBBox);

    overlap = intersectionArea(bbox1, bbox2);
    area = bboxArea(bbox1) + bboxArea(bbox2);

    // choose distribution with minimum overlap
    if (overlap < minOverlap) {
    minOverlap = overlap;
    index = i;

    minArea = area < minArea ? area : minArea;

    } else if (overlap === minOverlap) {
    // otherwise choose distribution with minimum area
    if (area < minArea) {
    minArea = area;
    index = i;
    }
    }
    }

    return index;
    },

    // sorts node children by the best axis for split
    _chooseSplitAxis: function (node, m, M) {

    var compareMinX = node.leaf ? this.compareMinX : compareNodeMinX,
    compareMinY = node.leaf ? this.compareMinY : compareNodeMinY,
    xMargin = this._allDistMargin(node, m, M, compareMinX),
    yMargin = this._allDistMargin(node, m, M, compareMinY);

    // if total distributions margin value is minimal for x, sort by minX,
    // otherwise it's already sorted by minY
    if (xMargin < yMargin) node.children.sort(compareMinX);
    },

    // total margin of all possible split distributions where each node is at least m full
    _allDistMargin: function (node, m, M, compare) {

    node.children.sort(compare);

    var toBBox = this.toBBox,
    leftBBox = distBBox(node, 0, m, toBBox),
    rightBBox = distBBox(node, M - m, M, toBBox),
    margin = bboxMargin(leftBBox) + bboxMargin(rightBBox),
    i, child;

    for (i = m; i < M - m; i++) {
    child = node.children[i];
    extend(leftBBox, node.leaf ? toBBox(child) : child.bbox);
    margin += bboxMargin(leftBBox);
    }

    for (i = M - m - 1; i >= m; i--) {
    child = node.children[i];
    extend(rightBBox, node.leaf ? toBBox(child) : child.bbox);
    margin += bboxMargin(rightBBox);
    }

    return margin;
    },

    _adjustParentBBoxes: function (bbox, path, level) {
    // adjust bboxes along the given tree path
    for (var i = level; i >= 0; i--) {
    extend(path[i].bbox, bbox);
    }
    },

    _condense: function (path) {
    // go through the path, removing empty nodes and updating bboxes
    for (var i = path.length - 1, siblings; i >= 0; i--) {
    if (path[i].children.length === 0) {
    if (i > 0) {
    siblings = path[i - 1].children;
    siblings.splice(siblings.indexOf(path[i]), 1);

    } else this.clear();

    } else calcBBox(path[i], this.toBBox);
    }
    },

    _initFormat: function (format) {
    // data format (minX, minY, maxX, maxY accessors)

    // uses eval-type function compilation instead of just accepting a toBBox function
    // because the algorithms are very sensitive to sorting functions performance,
    // so they should be dead simple and without inner calls

    // jshint evil: true

    var compareArr = ['return a', ' - b', ';'];

    this.compareMinX = new Function('a', 'b', compareArr.join(format[0]));
    this.compareMinY = new Function('a', 'b', compareArr.join(format[1]));

    this.toBBox = new Function('a', 'return [a' + format.join(', a') + '];');
    }
    };


    // calculate node's bbox from bboxes of its children
    function calcBBox(node, toBBox) {
    node.bbox = distBBox(node, 0, node.children.length, toBBox);
    }

    // min bounding rectangle of node children from k to p-1
    function distBBox(node, k, p, toBBox) {
    var bbox = empty();

    for (var i = k, child; i < p; i++) {
    child = node.children[i];
    extend(bbox, node.leaf ? toBBox(child) : child.bbox);
    }

    return bbox;
    }

    function empty() { return [Infinity, Infinity, -Infinity, -Infinity]; }

    function extend(a, b) {
    a[0] = Math.min(a[0], b[0]);
    a[1] = Math.min(a[1], b[1]);
    a[2] = Math.max(a[2], b[2]);
    a[3] = Math.max(a[3], b[3]);
    return a;
    }

    function compareNodeMinX(a, b) { return a.bbox[0] - b.bbox[0]; }
    function compareNodeMinY(a, b) { return a.bbox[1] - b.bbox[1]; }

    function bboxArea(a) { return (a[2] - a[0]) * (a[3] - a[1]); }
    function bboxMargin(a) { return (a[2] - a[0]) + (a[3] - a[1]); }

    function enlargedArea(a, b) {
    return (Math.max(b[2], a[2]) - Math.min(b[0], a[0])) *
    (Math.max(b[3], a[3]) - Math.min(b[1], a[1]));
    }

    function intersectionArea(a, b) {
    var minX = Math.max(a[0], b[0]),
    minY = Math.max(a[1], b[1]),
    maxX = Math.min(a[2], b[2]),
    maxY = Math.min(a[3], b[3]);

    return Math.max(0, maxX - minX) *
    Math.max(0, maxY - minY);
    }

    function contains(a, b) {
    return a[0] <= b[0] &&
    a[1] <= b[1] &&
    b[2] <= a[2] &&
    b[3] <= a[3];
    }

    function intersects(a, b) {
    return b[0] <= a[2] &&
    b[1] <= a[3] &&
    b[2] >= a[0] &&
    b[3] >= a[1];
    }

    // sort an array so that items come in groups of n unsorted items, with groups sorted between each other;
    // combines selection algorithm with binary divide & conquer approach

    function multiSelect(arr, left, right, n, compare) {
    var stack = [left, right],
    mid;

    while (stack.length) {
    right = stack.pop();
    left = stack.pop();

    if (right - left <= n) continue;

    mid = left + Math.ceil((right - left) / n / 2) * n;
    select(arr, left, right, mid, compare);

    stack.push(left, mid, mid, right);
    }
    }

    // Floyd-Rivest selection algorithm:
    // sort an array between left and right (inclusive) so that the smallest k elements come first (unordered)
    function select(arr, left, right, k, compare) {
    var n, i, z, s, sd, newLeft, newRight, t, j;

    while (right > left) {
    if (right - left > 600) {
    n = right - left + 1;
    i = k - left + 1;
    z = Math.log(n);
    s = 0.5 * Math.exp(2 * z / 3);
    sd = 0.5 * Math.sqrt(z * s * (n - s) / n) * (i - n / 2 < 0 ? -1 : 1);
    newLeft = Math.max(left, Math.floor(k - i * s / n + sd));
    newRight = Math.min(right, Math.floor(k + (n - i) * s / n + sd));
    select(arr, newLeft, newRight, k, compare);
    }

    t = arr[k];
    i = left;
    j = right;

    swap(arr, left, k);
    if (compare(arr[right], t) > 0) swap(arr, left, right);

    while (i < j) {
    swap(arr, i, j);
    i++;
    j--;
    while (compare(arr[i], t) < 0) i++;
    while (compare(arr[j], t) > 0) j--;
    }

    if (compare(arr[left], t) === 0) swap(arr, left, j);
    else {
    j++;
    swap(arr, j, right);
    }

    if (j <= k) left = j + 1;
    if (k <= j) right = j - 1;
    }
    }

    function swap(arr, i, j) {
    var tmp = arr[i];
    arr[i] = arr[j];
    arr[j] = tmp;
    }


    // export as AMD/CommonJS module or global variable
    if (typeof define === 'function' && define.amd) define('rbush', function () { return rbush; });
    else if (typeof module !== 'undefined') module.exports = rbush;
    else if (typeof self !== 'undefined') self.rbush = rbush;
    else window.rbush = rbush;

    })();
    653 changes: 653 additions & 0 deletions spam.js
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,653 @@
    var StaticCanvasMap;
    var ZoomableCanvasMap;

    ! function() {
    "use strict";

    // TODO use turf inside as a dependency?
    // Copied from turf.inside
    function inside(pt, polygon) {
    var polys = polygon.geometry.coordinates
    // normalize to multipolygon
    if (polygon.geometry.type === 'Polygon')
    polys = [polys]

    var insidePoly = false
    var i = 0
    while (i < polys.length && !insidePoly) {
    // check if it is in the outer ring first
    if (inRing(pt, polys[i][0])) {
    var inHole = false
    var k = 1
    // check for the point in any of the holes
    while (k < polys[i].length && !inHole) {
    if (inRing(pt, polys[i][k])) {
    inHole = true
    }
    k++
    }
    if(!inHole)
    insidePoly = true
    }
    i++
    }
    return insidePoly
    }

    // pt is [x,y] and ring is [[x,y], [x,y],..]
    function inRing (pt, ring) {
    var isInside = false
    for (var i = 0, j = ring.length - 1; i < ring.length; j = i++) {
    var xi = ring[i][0], yi = ring[i][1]
    var xj = ring[j][0], yj = ring[j][1]
    var intersect = ((yi > pt[1]) !== (yj > pt[1])) &&
    (pt[0] < (xj - xi) * (pt[1] - yi) / (yj - yi) + xi)
    if (intersect) isInside = !isInside
    }
    return isInside
    }

    function maxBounds(one, two) {
    var bounds = two
    if (one[0][0] < two[0][0])
    bounds[0][0] = one[0][0]
    if (one[0][1] < two[0][1])
    bounds[0][1] = one[0][1]
    if (one[1][0] > two[1][0])
    bounds[1][0] = one[1][0]
    if (one[1][1] > two[1][1])
    bounds[1][1] = one[1][1]
    return bounds
    }

    function createRTree(element, dataPath) {
    element.lookupTree = rbush(4)
    var elements = []

    for (var j in element.features.features) {
    var bounds = dataPath.bounds(element.features.features[j])
    elements.push([
    bounds[0][0].toFixed(0),
    bounds[0][1].toFixed(0),
    Math.ceil(bounds[1][0]),
    Math.ceil(bounds[1][1]),
    element.features.features[j]
    ])
    }
    element.lookupTree.load(elements)
    }

    function paintFeature(element, feature, parameters) {
    parameters.context.beginPath()
    parameters.path(feature)
    element.paintfeature(parameters, feature)
    }

    function paintBackgroundElement(element, parameters) {
    if (element.prepaint)
    element.prepaint(parameters)
    if (element.paintfeature) {
    var lookup = element.lookupTree.search([
    parameters.translate[0],
    parameters.translate[1],
    parameters.width / parameters.scale - parameters.translate[0],
    parameters.height / parameters.scale - parameters.translate[1]
    ])
    for (var j in lookup) {
    paintFeature(element, lookup[j][4], parameters)
    }
    }
    if (element.postpaint)
    element.postpaint(parameters)
    }

    function PartialPainter(data, parameters) {
    var index = 0,
    j = 0,
    element = null,
    currentLookup = []

    this.hasNext = function() {
    return index <= data.length && j < currentLookup.length
    }
    this.renderNext = function() {
    if (index >= data.length && j >= currentLookup.length)
    return
    var start = performance.now()
    if (!element || j >= currentLookup.length) {
    element = data[index]

    if (element.prepaint)
    element.prepaint(parameters)
    currentLookup = element.lookupTree.search([
    - parameters.translate[0],
    - parameters.translate[1],
    parameters.width / parameters.scale - parameters.translate[0],
    parameters.height / parameters.scale - parameters.translate[1]
    ])
    j = 0
    ++index
    }
    if (element.paintfeature) {
    for (; j != currentLookup.length; ++j) {
    var feature = currentLookup[j][4]
    paintFeature(element, feature, parameters)
    if ((performance.now() - start) > 10)
    break
    }
    } else {
    j = currentLookup.length
    }
    if (j == currentLookup.length && element.postpaint) {
    element.postpaint(parameters)
    }
    }
    this.finish = function() {
    if (index >= data.length && j >= currentLookup.length)
    return
    if (j < currentLookup.length)
    index--
    for (; index != data.length; ++index) {
    if (j >= currentLookup.length) {
    element = data[index]

    if (element.prepaint)
    element.prepaint(parameters)
    currentLookup = element.lookupTree.search([
    - parameters.translate[0],
    - parameters.translate[1],
    parameters.width / parameters.scale - parameters.translate[0],
    parameters.height / parameters.scale - parameters.translate[1]
    ])
    j = 0
    }
    for (; j != currentLookup.length; ++j) {
    var feature = currentLookup[j][4]
    paintFeature(element, feature, parameters)
    }
    if (element.postpaint)
    element.postpaint(parameters)
    }
    index = 0
    }
    }


    function translatePoint(point, scale, translate) {
    return [
    point[0] / scale - translate[0],
    point[1] / scale - translate[1]
    ]
    }

    function extend(extension, obj) {
    var newObj = {}
    // FIXME this is a bit hacky? Can't we just mutate the original obj? (can't bc projection)
    for (var elem in obj) {
    newObj[elem] = obj[elem]
    }
    for (var elem in extension) {
    if (!newObj.hasOwnProperty(elem))
    newObj[elem] = extension[elem]
    }
    return newObj
    }

    function CanvasMap(parameters) {
    var settings = extend({
    width: d3.select(parameters.element).node().getBoundingClientRect().width,
    ratio: 1,
    area: 0,
    scale: 1,
    translate: [0, 0],
    background: null,
    backgroundScale: 1,
    backgroundTranslate: [0, 0],
    map: this
    }, parameters),
    simplify = d3.geo.transform({
    point: function(x, y, z) {
    if (z >= settings.area) {
    this.stream.point(x, y)
    }
    }
    }),
    canvas = null,
    context = null

    if (!parameters.projection) {
    var b = [[Infinity, Infinity],
    [-Infinity, -Infinity]]
    for (var i in settings.data) {
    b = maxBounds(b, d3.geo.bounds(settings.data[i].features))
    }
    settings.projection = d3.geo.mercator()
    .scale(1)
    .center([(b[1][0] + b[0][0]) / 2, (b[1][1] + b[0][1]) / 2])
    }
    var dataPath = d3.geo.path().projection({
    stream: function(s) {
    return simplify.stream(settings.projection.stream(s))
    }
    })
    var b = [[Infinity, Infinity],
    [-Infinity, -Infinity]]
    for (var i in settings.data) {
    b = maxBounds(b, dataPath.bounds(settings.data[i].features))
    }

    var dx = b[1][0] - b[0][0],
    dy = b[1][1] - b[0][1]

    if (!parameters.projection) {
    settings.height = settings.height || Math.ceil(dy * settings.width / dx)
    settings.projection.scale(0.9 * (settings.width / dx))
    .translate([settings.width / 2, settings.height / 2])
    } else if (!settings.height) {
    settings.height = dy * 1 / 0.9
    }
    d3.select(settings.parameters).attr("height", settings.height)

    function init() {
    canvas = d3.select(settings.element)
    .append("canvas")
    context = canvas.node().getContext("2d")

    var devicePixelRatio = window.devicePixelRatio || 1,
    backingStoreRatio = context.webkitBackingStorePixelRatio ||
    context.mozBackingStorePixelRatio ||
    context.msBackingStorePixelRatio ||
    context.oBackingStorePixelRatio ||
    context.backingStorePixelRatio || 1
    settings.ratio = devicePixelRatio / backingStoreRatio
    settings.area = 1 / settings.projection.scale() / settings.ratio / 20

    canvas.attr("width", settings.width * settings.ratio)
    canvas.attr("height", settings.height * settings.ratio)
    canvas.style("width", settings.width + "px")
    canvas.style("height", settings.height + "px")
    context.lineJoin = "round"
    context.lineCap = "round"

    dataPath.context(context)
    context.clearRect(0, 0, settings.width * settings.ratio, settings.height * settings.ratio)
    context.save()
    context.scale(settings.ratio, settings.ratio)

    // TODO move rtree part out?
    for (var i in settings.data) {
    createRTree(settings.data[i], dataPath)
    }

    settings.background = new Image()
    settings.backgroundScale = settings.scale
    settings.backgroundTranslate = settings.translate
    var parameters = {
    path: dataPath,
    context: context,
    scale: settings.scale,
    translate: settings.translate,
    width: settings.width,
    height: settings.height,
    map: settings.map
    }
    var callback = function() {
    for (var i in settings.data) {
    var element = settings.data[i]

    if (element.dynamicpaint)
    element.dynamicpaint(parameters, null)
    }

    context.restore()
    canvas.on("click", click)
    .on("mousemove", hover)
    .on("mouseleave", hoverLeave)
    }
    for (var i in settings.data) {
    var element = settings.data[i]
    paintBackgroundElement(element, parameters)
    }
    settings.background.onload = callback
    settings.background.src = canvas.node().toDataURL()

    //Prevent another call to the init method
    this.init = function() {}
    }

    // TODO probably try to use the same data path in the zoom class, but have a different area settable?

    function paint() {
    context.save()
    context.scale(settings.scale * settings.ratio, settings.scale * settings.ratio)
    context.translate(settings.translate[0], settings.translate[1])

    context.clearRect(- settings.translate[0], - settings.translate[1], settings.width * settings.ratio, settings.height * settings.ratio)

    context.rect(- settings.translate[0], - settings.translate[1],
    settings.width / settings.scale,
    settings.height / settings.scale)
    context.clip()

    context.drawImage(settings.background, 0, 0,
    settings.width * settings.ratio, settings.height * settings.ratio,
    - settings.backgroundTranslate[0],
    - settings.backgroundTranslate[1],
    settings.width / settings.backgroundScale, settings.height / settings.backgroundScale)

    // FIXME this needs a way for the callback to use the lookupTree?
    var parameters = {
    path: dataPath,
    context: dataPath.context(),
    scale: settings.scale,
    translate: settings.translate,
    width: settings.width,
    height: settings.height,
    map: settings.map
    }
    settings.area = 1 / settings.projection.scale() / settings.scale / settings.ratio / 20
    for (var i in settings.data) {
    var element = settings.data[i]
    if (element.dynamicpaint)
    element.dynamicpaint(parameters, element.hoverElement)
    }

    context.restore()
    }

    function click() {
    var point = translatePoint(d3.mouse(this), settings.scale, settings.translate)

    var parameters = {
    path: dataPath,
    context: context,
    scale: settings.scale,
    translate: settings.translate,
    width: settings.width,
    height: settings.height,
    map: settings.map
    }
    for (var i in settings.data) {
    var element = settings.data[i]
    if (!element.click)
    continue

    var lookup = element.lookupTree.search([point[0], point[1], point[0], point[1]])
    var isInside = false
    for (var j in lookup) {
    var feature = lookup[j][4]
    if (inside(settings.projection.invert(point), feature)) {
    element.click(parameters, feature)
    isInside = true
    }
    }
    isInside || element.click(parameters, null)
    }
    }

    function hoverLeave() {
    for (var i in settings.data) {
    var element = settings.data[i]
    element.hoverElement = false
    }
    paint()
    }

    function hover() {
    var point = translatePoint(d3.mouse(this), settings.scale, settings.translate),
    repaint = false

    for (var i in settings.data) {
    var element = settings.data[i]
    if (element.hoverElement &&
    inside(settings.projection.invert(point), element.hoverElement)) {
    continue
    }
    element.hoverElement = false
    var lookup = element.lookupTree.search([point[0], point[1], point[0], point[1]])
    for (var j in lookup) {
    var feature = lookup[j][4]
    if (inside(settings.projection.invert(point), feature)) {
    element.hoverElement = feature
    break
    }
    }
    repaint = true
    }
    repaint && paint()
    }

    this.init = init
    this.paint = paint
    this.settings = function() {
    return settings
    }
    }

    StaticCanvasMap = function(parameters) {
    var map = new CanvasMap(parameters)

    this.init = function() {
    map.init()
    }
    this.paint = function() {
    map.paint()
    }
    }

    var epsilon = 0.5
    function nearEqual(a, b) {
    return Math.abs(a - b) < epsilon
    }

    function ImageCache(parameters) {
    var cache = [],
    settings = parameters

    this.addImage = function(parameters) {
    cache.push(parameters)
    }

    this.getImage = function(parameters) {
    for (var i in cache) {
    var element = cache[i]
    if (nearEqual(element.scale, parameters.scale) &&
    nearEqual(element.translate[0], parameters.translate[0]) &&
    nearEqual(element.translate[1], parameters.translate[1]))
    return element
    }
    return null
    }

    this.getFittingImage = function(bbox) {
    // Auto set scale=1, translate[0, 0] image as default return
    var currentImage = cache.length > 0 ? cache[0] : null
    for (var i in cache) {
    var image = cache[i]
    var imageBB = [
    - image.translate[0],
    - image.translate[1],
    settings.width / image.scale - image.translate[0],
    settings.height / image.scale - image.translate[1]
    ]
    if (imageBB[0] <= bbox[0] &&
    imageBB[1] <= bbox[1] &&
    imageBB[2] >= bbox[2] &&
    imageBB[3] >= bbox[3] &&
    (!currentImage || currentImage.scale < image.scale)) {
    currentImage = image
    }
    }
    return currentImage
    }
    }

    ZoomableCanvasMap = function(parameters) {
    var map = new CanvasMap(parameters),
    simplify = d3.geo.transform({
    point: function(x, y, z) {
    if (z >= area) this.stream.point(x, y)
    }
    }),
    area = 0,
    canvas = null,
    context = null,
    settings = map.settings(),
    dataPath = d3.geo.path().projection({
    stream: function(s) {
    return simplify.stream(settings.projection.stream(s))
    }
    }),
    imageCache = new ImageCache({
    width: settings.width,
    height: settings.height
    })

    settings.map = this
    settings.zoomScaleFactor = settings.zoomScaleFactor || 0.5

    this.init = function() {
    map.init()

    canvas = d3.select(settings.element)
    .append("canvas")
    context = canvas.node().getContext("2d")
    area = 1 / settings.projection.scale() / settings.ratio / 20

    canvas.attr("width", settings.width * settings.ratio)
    canvas.attr("height", settings.height * settings.ratio)
    canvas.style("width", settings.width + "px")
    canvas.style("height", settings.height + "px")
    canvas.style("display", "none")
    context.lineJoin = "round"
    context.lineCap = "round"

    dataPath.context(context)

    imageCache.addImage({
    image: settings.background,
    scale: settings.scale,
    translate: settings.translate
    })
    }
    this.paint = function() {
    map.paint()
    }
    function scaleZoom(scale, translate) {
    if (nearEqual(scale, settings.scale) &&
    nearEqual(translate[0], settings.translate[0]) &&
    nearEqual(translate[1], settings.translate[1])) {
    scale = 1
    translate = [0, 0]
    }
    if (scale == 1 && settings.scale == 1 &&
    !translate[0] && !translate[1] &&
    !settings.translate[0] && !settings.translate[1]) {
    return
    }
    area = 1 / settings.projection.scale() / scale / settings.ratio / 20

    context.save()
    context.scale(scale * settings.ratio, scale * settings.ratio)
    context.translate(translate[0], translate[1])
    context.clearRect(- translate[0], - translate[1], settings.width * settings.ratio, settings.height * settings.ratio)
    var parameters = {
    path: dataPath,
    context: context,
    scale: scale,
    translate: translate,
    width: settings.width,
    height: settings.height,
    map: settings.map
    }

    var image = imageCache.getImage({
    scale: scale,
    translate: translate
    })
    if (!image) {
    var background = new Image(),
    partialPainter = new PartialPainter(settings.data, parameters)
    }

    var translatedOne = translatePoint([settings.width, settings.height], scale, translate),
    translatedTwo = translatePoint([settings.width, settings.height], settings.scale, settings.translate)
    var bbox = [
    Math.min(- translate[0], - settings.translate[0]),
    Math.min(- translate[1], - settings.translate[1]),
    Math.max(translatedOne[0], translatedTwo[0]),
    Math.max(translatedOne[1], translatedTwo[1])
    ]
    var zoomImage = imageCache.getFittingImage(bbox)
    if (zoomImage) {
    settings.background = zoomImage.image
    settings.backgroundScale = zoomImage.scale
    settings.backgroundTranslate = zoomImage.translate
    }
    d3.transition()
    .duration(300)
    .ease("linear")
    .tween("zoom", function() {
    var i = d3.interpolateNumber(settings.scale, scale),
    oldTranslate = settings.translate,
    oldScale = settings.scale
    return function(t) {
    settings.scale = i(t)
    var newTranslate = [
    oldTranslate[0] + (translate[0] - oldTranslate[0]) / (scale - oldScale) * (i(t) - oldScale) * scale / i(t),
    oldTranslate[1] + (translate[1] - oldTranslate[1]) / (scale - oldScale) * (i(t) - oldScale) * scale / i(t),
    ]
    settings.translate = newTranslate
    map.paint()
    !image && partialPainter.renderNext()
    }
    })
    .each("end", function() {
    settings.scale = scale
    settings.translate = translate

    if (image) {
    context.restore()
    settings.background = image.image
    settings.backgroundScale = image.scale
    settings.backgroundTranslate = image.translate
    map.paint()
    } else {
    partialPainter.finish()
    background.onload = function() {
    context.restore()
    imageCache.addImage({
    image: background,
    scale: scale,
    translate: translate
    })
    settings.background = background
    settings.backgroundScale = scale
    settings.backgroundTranslate = translate
    map.paint()
    }
    // TODO there is a function to get the image data from the context, is that faster?
    // TODO use getImageData/putImageData, because it's faster?
    background.src = canvas.node().toDataURL()
    }
    })
    }
    this.zoom = function(d) {
    if (!d) {
    scaleZoom.call(this, 1, [0, 0])
    return
    }
    var bounds = dataPath.bounds(d),
    dx = bounds[1][0] - bounds[0][0],
    dy = bounds[1][1] - bounds[0][1],
    bx = (bounds[0][0] + bounds[1][0]) / 2,
    by = (bounds[0][1] + bounds[1][1]) / 2,
    scale = settings.zoomScaleFactor *
    Math.min(settings.width / dx, settings.height / dy),
    translate = [-bx + settings.width / scale / 2,
    -by + settings.height / scale / 2]

    scaleZoom.call(this, scale, translate)
    }
    }
    }()
    538 changes: 538 additions & 0 deletions topojson.js
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,538 @@
    !function() {
    var topojson = {
    version: "1.6.19",
    mesh: function(topology) { return object(topology, meshArcs.apply(this, arguments)); },
    meshArcs: meshArcs,
    merge: function(topology) { return object(topology, mergeArcs.apply(this, arguments)); },
    mergeArcs: mergeArcs,
    feature: featureOrCollection,
    neighbors: neighbors,
    presimplify: presimplify
    };

    function stitchArcs(topology, arcs) {
    var stitchedArcs = {},
    fragmentByStart = {},
    fragmentByEnd = {},
    fragments = [],
    emptyIndex = -1;

    // Stitch empty arcs first, since they may be subsumed by other arcs.
    arcs.forEach(function(i, j) {
    var arc = topology.arcs[i < 0 ? ~i : i], t;
    if (arc.length < 3 && !arc[1][0] && !arc[1][1]) {
    t = arcs[++emptyIndex], arcs[emptyIndex] = i, arcs[j] = t;
    }
    });

    arcs.forEach(function(i) {
    var e = ends(i),
    start = e[0],
    end = e[1],
    f, g;

    if (f = fragmentByEnd[start]) {
    delete fragmentByEnd[f.end];
    f.push(i);
    f.end = end;
    if (g = fragmentByStart[end]) {
    delete fragmentByStart[g.start];
    var fg = g === f ? f : f.concat(g);
    fragmentByStart[fg.start = f.start] = fragmentByEnd[fg.end = g.end] = fg;
    } else {
    fragmentByStart[f.start] = fragmentByEnd[f.end] = f;
    }
    } else if (f = fragmentByStart[end]) {
    delete fragmentByStart[f.start];
    f.unshift(i);
    f.start = start;
    if (g = fragmentByEnd[start]) {
    delete fragmentByEnd[g.end];
    var gf = g === f ? f : g.concat(f);
    fragmentByStart[gf.start = g.start] = fragmentByEnd[gf.end = f.end] = gf;
    } else {
    fragmentByStart[f.start] = fragmentByEnd[f.end] = f;
    }
    } else {
    f = [i];
    fragmentByStart[f.start = start] = fragmentByEnd[f.end = end] = f;
    }
    });

    function ends(i) {
    var arc = topology.arcs[i < 0 ? ~i : i], p0 = arc[0], p1;
    if (topology.transform) p1 = [0, 0], arc.forEach(function(dp) { p1[0] += dp[0], p1[1] += dp[1]; });
    else p1 = arc[arc.length - 1];
    return i < 0 ? [p1, p0] : [p0, p1];
    }

    function flush(fragmentByEnd, fragmentByStart) {
    for (var k in fragmentByEnd) {
    var f = fragmentByEnd[k];
    delete fragmentByStart[f.start];
    delete f.start;
    delete f.end;
    f.forEach(function(i) { stitchedArcs[i < 0 ? ~i : i] = 1; });
    fragments.push(f);
    }
    }

    flush(fragmentByEnd, fragmentByStart);
    flush(fragmentByStart, fragmentByEnd);
    arcs.forEach(function(i) { if (!stitchedArcs[i < 0 ? ~i : i]) fragments.push([i]); });

    return fragments;
    }

    function meshArcs(topology, o, filter) {
    var arcs = [];

    if (arguments.length > 1) {
    var geomsByArc = [],
    geom;

    function arc(i) {
    var j = i < 0 ? ~i : i;
    (geomsByArc[j] || (geomsByArc[j] = [])).push({i: i, g: geom});
    }

    function line(arcs) {
    arcs.forEach(arc);
    }

    function polygon(arcs) {
    arcs.forEach(line);
    }

    function geometry(o) {
    if (o.type === "GeometryCollection") o.geometries.forEach(geometry);
    else if (o.type in geometryType) geom = o, geometryType[o.type](o.arcs);
    }

    var geometryType = {
    LineString: line,
    MultiLineString: polygon,
    Polygon: polygon,
    MultiPolygon: function(arcs) { arcs.forEach(polygon); }
    };

    geometry(o);

    geomsByArc.forEach(arguments.length < 3
    ? function(geoms) { arcs.push(geoms[0].i); }
    : function(geoms) { if (filter(geoms[0].g, geoms[geoms.length - 1].g)) arcs.push(geoms[0].i); });
    } else {
    for (var i = 0, n = topology.arcs.length; i < n; ++i) arcs.push(i);
    }

    return {type: "MultiLineString", arcs: stitchArcs(topology, arcs)};
    }

    function mergeArcs(topology, objects) {
    var polygonsByArc = {},
    polygons = [],
    components = [];

    objects.forEach(function(o) {
    if (o.type === "Polygon") register(o.arcs);
    else if (o.type === "MultiPolygon") o.arcs.forEach(register);
    });

    function register(polygon) {
    polygon.forEach(function(ring) {
    ring.forEach(function(arc) {
    (polygonsByArc[arc = arc < 0 ? ~arc : arc] || (polygonsByArc[arc] = [])).push(polygon);
    });
    });
    polygons.push(polygon);
    }

    function exterior(ring) {
    return cartesianRingArea(object(topology, {type: "Polygon", arcs: [ring]}).coordinates[0]) > 0; // TODO allow spherical?
    }

    polygons.forEach(function(polygon) {
    if (!polygon._) {
    var component = [],
    neighbors = [polygon];
    polygon._ = 1;
    components.push(component);
    while (polygon = neighbors.pop()) {
    component.push(polygon);
    polygon.forEach(function(ring) {
    ring.forEach(function(arc) {
    polygonsByArc[arc < 0 ? ~arc : arc].forEach(function(polygon) {
    if (!polygon._) {
    polygon._ = 1;
    neighbors.push(polygon);
    }
    });
    });
    });
    }
    }
    });

    polygons.forEach(function(polygon) {
    delete polygon._;
    });

    return {
    type: "MultiPolygon",
    arcs: components.map(function(polygons) {
    var arcs = [];

    // Extract the exterior (unique) arcs.
    polygons.forEach(function(polygon) {
    polygon.forEach(function(ring) {
    ring.forEach(function(arc) {
    if (polygonsByArc[arc < 0 ? ~arc : arc].length < 2) {
    arcs.push(arc);
    }
    });
    });
    });

    // Stitch the arcs into one or more rings.
    arcs = stitchArcs(topology, arcs);

    // If more than one ring is returned,
    // at most one of these rings can be the exterior;
    // this exterior ring has the same winding order
    // as any exterior ring in the original polygons.
    if ((n = arcs.length) > 1) {
    var sgn = exterior(polygons[0][0]);
    for (var i = 0, t; i < n; ++i) {
    if (sgn === exterior(arcs[i])) {
    t = arcs[0], arcs[0] = arcs[i], arcs[i] = t;
    break;
    }
    }
    }

    return arcs;
    })
    };
    }

    function featureOrCollection(topology, o) {
    return o.type === "GeometryCollection" ? {
    type: "FeatureCollection",
    features: o.geometries.map(function(o) { return feature(topology, o); })
    } : feature(topology, o);
    }

    function feature(topology, o) {
    var f = {
    type: "Feature",
    id: o.id,
    properties: o.properties || {},
    geometry: object(topology, o)
    };
    if (o.id == null) delete f.id;
    return f;
    }

    function object(topology, o) {
    var absolute = transformAbsolute(topology.transform),
    arcs = topology.arcs;

    function arc(i, points) {
    if (points.length) points.pop();
    for (var a = arcs[i < 0 ? ~i : i], k = 0, n = a.length, p; k < n; ++k) {
    points.push(p = a[k].slice());
    absolute(p, k);
    }
    if (i < 0) reverse(points, n);
    }

    function point(p) {
    p = p.slice();
    absolute(p, 0);
    return p;
    }

    function line(arcs) {
    var points = [];
    for (var i = 0, n = arcs.length; i < n; ++i) arc(arcs[i], points);
    if (points.length < 2) points.push(points[0].slice());
    return points;
    }

    function ring(arcs) {
    var points = line(arcs);
    while (points.length < 4) points.push(points[0].slice());
    return points;
    }

    function polygon(arcs) {
    return arcs.map(ring);
    }

    function geometry(o) {
    var t = o.type;
    return t === "GeometryCollection" ? {type: t, geometries: o.geometries.map(geometry)}
    : t in geometryType ? {type: t, coordinates: geometryType[t](o)}
    : null;
    }

    var geometryType = {
    Point: function(o) { return point(o.coordinates); },
    MultiPoint: function(o) { return o.coordinates.map(point); },
    LineString: function(o) { return line(o.arcs); },
    MultiLineString: function(o) { return o.arcs.map(line); },
    Polygon: function(o) { return polygon(o.arcs); },
    MultiPolygon: function(o) { return o.arcs.map(polygon); }
    };

    return geometry(o);
    }

    function reverse(array, n) {
    var t, j = array.length, i = j - n; while (i < --j) t = array[i], array[i++] = array[j], array[j] = t;
    }

    function bisect(a, x) {
    var lo = 0, hi = a.length;
    while (lo < hi) {
    var mid = lo + hi >>> 1;
    if (a[mid] < x) lo = mid + 1;
    else hi = mid;
    }
    return lo;
    }

    function neighbors(objects) {
    var indexesByArc = {}, // arc index -> array of object indexes
    neighbors = objects.map(function() { return []; });

    function line(arcs, i) {
    arcs.forEach(function(a) {
    if (a < 0) a = ~a;
    var o = indexesByArc[a];
    if (o) o.push(i);
    else indexesByArc[a] = [i];
    });
    }

    function polygon(arcs, i) {
    arcs.forEach(function(arc) { line(arc, i); });
    }

    function geometry(o, i) {
    if (o.type === "GeometryCollection") o.geometries.forEach(function(o) { geometry(o, i); });
    else if (o.type in geometryType) geometryType[o.type](o.arcs, i);
    }

    var geometryType = {
    LineString: line,
    MultiLineString: polygon,
    Polygon: polygon,
    MultiPolygon: function(arcs, i) { arcs.forEach(function(arc) { polygon(arc, i); }); }
    };

    objects.forEach(geometry);

    for (var i in indexesByArc) {
    for (var indexes = indexesByArc[i], m = indexes.length, j = 0; j < m; ++j) {
    for (var k = j + 1; k < m; ++k) {
    var ij = indexes[j], ik = indexes[k], n;
    if ((n = neighbors[ij])[i = bisect(n, ik)] !== ik) n.splice(i, 0, ik);
    if ((n = neighbors[ik])[i = bisect(n, ij)] !== ij) n.splice(i, 0, ij);
    }
    }
    }

    return neighbors;
    }

    function presimplify(topology, triangleArea) {
    var absolute = transformAbsolute(topology.transform),
    relative = transformRelative(topology.transform),
    heap = minAreaHeap();

    if (!triangleArea) triangleArea = cartesianTriangleArea;

    topology.arcs.forEach(function(arc) {
    var triangles = [],
    maxArea = 0,
    triangle;

    // To store each point’s effective area, we create a new array rather than
    // extending the passed-in point to workaround a Chrome/V8 bug (getting
    // stuck in smi mode). For midpoints, the initial effective area of
    // Infinity will be computed in the next step.
    for (var i = 0, n = arc.length, p; i < n; ++i) {
    p = arc[i];
    arc[i] = [p[0], p[1], Infinity, p[0], p[1]]
    absolute(arc[i], i);
    }

    for (var i = 1, n = arc.length - 1; i < n; ++i) {
    triangle = arc.slice(i - 1, i + 2);
    triangle[1][2] = triangleArea(triangle);
    triangles.push(triangle);
    heap.push(triangle);
    }

    for (var i = 0, n = triangles.length; i < n; ++i) {
    triangle = triangles[i];
    triangle.previous = triangles[i - 1];
    triangle.next = triangles[i + 1];
    }

    while (triangle = heap.pop()) {
    var previous = triangle.previous,
    next = triangle.next;

    // If the area of the current point is less than that of the previous point
    // to be eliminated, use the latter's area instead. This ensures that the
    // current point cannot be eliminated without eliminating previously-
    // eliminated points.
    if (triangle[1][2] < maxArea) triangle[1][2] = maxArea;
    else maxArea = triangle[1][2];

    if (previous) {
    previous.next = next;
    previous[2] = triangle[2];
    update(previous);
    }

    if (next) {
    next.previous = previous;
    next[0] = triangle[0];
    update(next);
    }
    }

    for (var i = 0, n = arc.length, p; i < n; ++i) {
    p = arc[i];
    arc[i] = [p[3], p[4], p[2]]
    }
    });

    function update(triangle) {
    heap.remove(triangle);
    triangle[1][2] = triangleArea(triangle);
    heap.push(triangle);
    }

    return topology;
    };

    function cartesianRingArea(ring) {
    var i = -1,
    n = ring.length,
    a,
    b = ring[n - 1],
    area = 0;

    while (++i < n) {
    a = b;
    b = ring[i];
    area += a[0] * b[1] - a[1] * b[0];
    }

    return area * .5;
    }

    function cartesianTriangleArea(triangle) {
    var a = triangle[0], b = triangle[1], c = triangle[2];
    return Math.abs((a[0] - c[0]) * (b[1] - a[1]) - (a[0] - b[0]) * (c[1] - a[1]));
    }

    function compareArea(a, b) {
    return a[1][2] - b[1][2];
    }

    function minAreaHeap() {
    var heap = {},
    array = [],
    size = 0;

    heap.push = function(object) {
    up(array[object._ = size] = object, size++);
    return size;
    };

    heap.pop = function() {
    if (size <= 0) return;
    var removed = array[0], object;
    if (--size > 0) object = array[size], down(array[object._ = 0] = object, 0);
    return removed;
    };

    heap.remove = function(removed) {
    var i = removed._, object;
    if (array[i] !== removed) return; // invalid request
    if (i !== --size) object = array[size], (compareArea(object, removed) < 0 ? up : down)(array[object._ = i] = object, i);
    return i;
    };

    function up(object, i) {
    while (i > 0) {
    var j = ((i + 1) >> 1) - 1,
    parent = array[j];
    if (compareArea(object, parent) >= 0) break;
    array[parent._ = i] = parent;
    array[object._ = i = j] = object;
    }
    }

    function down(object, i) {
    while (true) {
    var r = (i + 1) << 1,
    l = r - 1,
    j = i,
    child = array[j];
    if (l < size && compareArea(array[l], child) < 0) child = array[j = l];
    if (r < size && compareArea(array[r], child) < 0) child = array[j = r];
    if (j === i) break;
    array[child._ = i] = child;
    array[object._ = i = j] = object;
    }
    }

    return heap;
    }

    function transformAbsolute(transform) {
    if (!transform) return noop;
    var x0,
    y0,
    kx = transform.scale[0],
    ky = transform.scale[1],
    dx = transform.translate[0],
    dy = transform.translate[1];
    return function(point, i) {
    if (!i) x0 = y0 = 0;
    point[0] = (x0 += point[0]) * kx + dx;
    point[1] = (y0 += point[1]) * ky + dy;
    };
    }

    function transformRelative(transform) {
    if (!transform) return noop;
    var x0,
    y0,
    kx = transform.scale[0],
    ky = transform.scale[1],
    dx = transform.translate[0],
    dy = transform.translate[1];
    return function(point, i) {
    if (!i) x0 = y0 = 0;
    var x1 = (point[0] - dx) / kx | 0,
    y1 = (point[1] - dy) / ky | 0;
    point[0] = x1 - x0;
    point[1] = y1 - y0;
    x0 = x1;
    y0 = y1;
    };
    }

    function noop() {}

    if (typeof define === "function" && define.amd) define(topojson);
    else if (typeof module === "object" && module.exports) module.exports = topojson;
    else this.topojson = topojson;
    }();