var CircleController = can.Control({
isMobile: navigator.userAgent.match(/Mobile/i),
init: function () {
var me = this;
this.eventCount = 0;
// an array of circle objects
this.data = [];
var fragment = can.view('/templates/circle.ejs');
this.element.html(fragment);
this.setupDrawingArea();
this.setupWebsocket();
var throttleResize = _.throttle(function (info) { me.socket.emit('resize', info); }, 25);
this.element.bind('mousewheel', function(event, delta, deltaX, deltaY) {
if (event.target.nodeName == 'circle')
throttleResize({id: event.target.getAttribute('id'), delta: delta});
event.preventDefault();
});
},
setupDrawingArea: function () {
var me = this;
// setup drawing area
this.element.css("width", "450px").css("height", "450px");
this.scene = d3.select(this.element.find("svg")[0])
.attr("width", 500).attr("height", 500)
.on("click", function() {
var target = d3.event.target;
if (target.nodeName == 'circle') {
var id = d3.select(target).attr("id");
me.socket.emit('remove', id);
} else {
var pos = d3.mouse(this);
me.socket.emit('add', {x: pos[0]|0, y: pos[1]|0});
}
});
},
enter: function () {
this.dragging = false;
var me = this;
function dragStart() {
me.dragging = true;
var circle = d3.select(this);
var id = circle.attr("id");
me.socket.emit('moveStart', id);
};
function dragMove() {
var circle = d3.select(this);
var ev = d3.event;
circle.attr("cx", ev.dx + parseInt(circle.attr("cx")));
circle.attr("cy", ev.dy + parseInt(circle.attr("cy")));
var id = circle.attr("id");
var x = circle.attr('cx');
var y = circle.attr('cy');
me.socket.emit('move', {id: id, x: x, y: y});
};
function dragEnd() {
me.dragging = false;
var circle = d3.select(this);
var id = circle.attr("id");
var x = Math.min(450, Math.max(0, circle.attr('cx')));
var y = Math.min(450, Math.max(0, circle.attr('cy')));
me.socket.emit('moveEnd', {id: id, x: x, y: y});
};
// macro function
function P(propName) { return function(d) { return d[propName]; }; }
// the key function binds the data entries to the DOM elements using the id
var circles = this.scene.selectAll("circle.default").data(this.data, P("id"));
circles.enter() // the enter set, a set of new data entries without DOM equivalent
.append("circle")
.call(d3.behavior.drag()
.on('dragstart', dragStart)
.on('drag', dragMove)
.on('dragend', dragEnd))
.attr("id", P("id"))
.attr("class", "default")
.attr("r", 1)
.attr("cx", P("x"))
.attr("cy", P("y"))
.style("fill", function(d) { return me.isMobile ? d.color : "url(#gradient_" + d.color + ")"; })
.style("fill-opacity", P("opacity"))
.transition().duration(500)
.attr("r", P("r"));
},
exit: function () {
// macro function
function P(propName) { return function(d) { return d[propName]; }; }
var circles = this.scene.selectAll("circle.default").data(this.data, P("id"));
circles.exit() // the exit set, a set of superfluous DOM nodes without data equivalent
.attr("class", null)
.style("stroke-opacity", 0)
.transition()
.attr("r", function(d) { return 1.8*d.r; })
.style("fill-opacity", 0.05)
.each("end", function () { d3.select(this).remove(); });
},
update: function (dur) {
// macro function
function P(propName) { return function(d) { return d[propName]; }; }
var circles = this.scene.selectAll("circle.default").data(this.data, P("id"));
circles // the update set, all DOM nodes with a data binding
.transition().duration(dur)
.attr("r", P("r"))
.attr("cx", P("x"))
.attr("cy", P("y"));
},
//
// the payload called as event from server
//
setData: function (ts, circles) {
this.data = JSON.parse(JSON.stringify(circles));
this.exit();
this.enter();
this.update(800);
this.log(ts, this.data.length + " circles updated");
},
addData: function (ts, entry) {
this.data.push(JSON.parse(JSON.stringify(entry)));
this.enter();
this.log(ts, "circle added, id=" + entry.id);
},
toForeground: function (d3Node) {
var node = d3Node[0][0];
if (!node)
return;
node.parentNode.appendChild(node);
},
moveStart: function (ts, id) {
if (this.dragging)
return;
var circle = this.scene.select('circle[id="' + id + '"]');
circle.style("stroke", "white");
this.toForeground(circle);
},
moveEnd: function (ts, entry) {
this.log(ts, "circle moved, id=" + entry.id);
for (var i=0; i < this.data.length; i++) {
if (this.data[i].id == entry.id) {
this.data[i].x = entry.x;
this.data[i].y = entry.y;
break;
}
}
var circle = this.scene.select('circle[id="' + entry.id + '"]');
circle.style("stroke", null);
this.toForeground(circle);
this.update(300);
},
move: function (ts, entry) {
if (this.dragging)
return;
this.log(ts, "circle moving, id=" + entry.id);
var circle = this.scene.select('circle[id="' + entry.id+ '"]');
circle.attr("cx", entry.x).attr("cy", entry.y);
},
resize: function (ts, entry) {
this.log(ts, "circle resize, id=" + entry.id);
for (var i=0; i < this.data.length; i++) {
if (this.data[i].id == entry.id) {
this.data[i].r = entry.r;
break;
}
}
var circle = this.scene.select('circle[id="' + entry.id+ '"]');
circle.attr("r", entry.r);
},
removeData: function (ts, id) {
for (var i=0; i < this.data.length; i++) {
if (this.data[i].id == id) {
this.data.splice(i, 1);
break;
}
}
this.exit();
this.log(ts, "circle removed, id=" + id);
},
format: d3.time.format("%X"),
log: function (ts, msg) {
var fragment = this.eventCount + " circle events, " + msg + ' @' + this.format(new Date(ts)) + ' +' + ts%1000 + 'ms';
$('#log').html(fragment);
},
setupWebsocket: function () {
var me = this;
// map events to functions
var behaviour = {
'update': function (data) {
me.eventCount++;
me.setData(data.ts, data.circles);
},
'add': function (data) {
me.eventCount++;
me.addData(data.ts, data.entry);
},
'resize': function (data) {
me.eventCount++;
me.resize(data.ts, data.entry);
},
'remove': function (data) {
me.eventCount++;
me.removeData(data.ts, data.id);
},
'moveStart': function (data) {
me.eventCount++;
me.moveStart(data.ts, data.id);
},
'move': function (data) {
me.eventCount++;
me.move(data.ts, data.entry);
},
'moveEnd': function (data) {
me.eventCount++;
me.moveEnd(data.ts, data.entry);
}
};
// connect with websocket server using namespace: '/circle'
this.socket = io.connect('/circle');
_.map(behaviour, function(func, eventName) {
me.socket.on(eventName, func);
});
}
});