Skip to content

Instantly share code, notes, and snippets.

@rveciana
Last active May 26, 2016 12:24
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save rveciana/47d7ad338b97162ef9ab7d0bf00fe824 to your computer and use it in GitHub Desktop.
Save rveciana/47d7ad338b97162ef9ab7d0bf00fe824 to your computer and use it in GitHub Desktop.
spam.js with authomatic color selection
<!DOCTYPE html>
<meta charset="utf-8">
<body>
<script src="http://d3js.org/d3.v3.min.js"></script>
<script src="http://d3js.org/d3.geo.projection.v0.min.js"></script>
<script src="http://d3js.org/topojson.v1.min.js"></script>
<script src="spam.js"></script>
<script src="rbush.min.js"></script>
<script>
var width = 960,
height = 600
var i = 0
var color = d3.scale.category20()
var graticule = d3.geo.graticule()
// Canvas radial gradient
var x = width / 2,
y = height / 2,
// Radii of the start glow.
innerRadius = 220,
// Radii of the end glow.
outerRadius = 300,
// Radius of the entire circle.
radius = 200
d3.json("world-50m.json", function(error, d) {
topojson.presimplify(d)
var neighbors = topojson.neighbors(d.objects.countries.geometries)
var countries = topojson.feature(d, d.objects.countries)
countries.features.forEach(function(d, i) {
d.properties.color = d3.max(neighbors[i], function(n) { return countries.features[n].properties.color; }) + 1|0
});
var map = new StaticCanvasMap({
element: "body",
width: width,
height: height,
projection: d3.geo.orthographic()
.clipAngle(90)
.precision(0.1)
.scale(250),
data: [
{
features: countries,
static: {
prepaint: function(parameters) {
parameters.context.beginPath()
parameters.path({type: "Sphere"})
parameters.context.lineWidth = 2
parameters.context.strokeStyle = "rgb(198, 197, 197)"
parameters.context.stroke()
var gradient = parameters.context.createRadialGradient(x, y, innerRadius, x, y, outerRadius)
gradient.addColorStop(0, 'rgb(248, 248, 248)')
gradient.addColorStop(1, 'rgb(210, 210, 210)')
parameters.context.fillStyle = gradient
parameters.context.fill()
parameters.context.beginPath()
parameters.path(graticule())
parameters.context.lineWidth = 0.2
parameters.context.strokeStyle = 'rgba(30,30,30, 0.5)'
parameters.context.stroke()
},
paintfeature: function(parameters, d) {
parameters.context.lineWidth = 0.8 / parameters.scale
parameters.context.strokeStyle = "rgba(0,0,0, 0.6)"
parameters.context.stroke()
parameters.context.fillStyle = color(d.properties.color)
parameters.context.fill()
}
}
}
]
})
map.init()
})
</script>
(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})();
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.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 = {}
// 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() {
var hasHover = false,
hasClick = false
for (var i in settings.data) {
var element = settings.data[i]
hasHover = hasHover || (element.events && element.events.hover)
hasClick = hasClick || (element.events && element.events.click)
if (element.dynamic && element.dynamic.postpaint)
element.dynamic.postpaint(parameters, null)
}
context.restore()
hasClick && canvas.on("click", click)
hasHover && canvas.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()
//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()
// 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.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 = {
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.events || !element.events.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.events.click(parameters, feature)
isInside = true
}
}
isInside || element.events.click(parameters, null)
}
}
function hoverLeave() {
var parameters = {
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.events || !element.events.hover)
continue
element.hoverElement = false
element.events.hover(parameters, null)
}
}
function hover() {
var point = translatePoint(d3.mouse(this), settings.scale, settings.translate),
parameters = {
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.events || !element.events.hover ||
(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
}
}
element.events.hover(parameters, element.hoverElement)
}
}
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
}),
busy = false
settings.map = this
settings.zoomScale = settings.zoomScale || 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 (busy) {
return
}
busy = true
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 {
map.paint()
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()
}
busy = false
})
}
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)
}
this.settings = function(d){
return map.settings()
}
}
}()
Display the source blob
Display the rendered blob
Raw
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment