(function() { //http://stackoverflow.com/a/901144/678708 function getParameterByName(name) { name = name.replace(/[\[]/, "\\\[").replace(/[\]]/, "\\\]"); var regexS = "[\\?&]" + name + "=([^&#]*)"; var regex = new RegExp(regexS); var results = regex.exec(parent.window.location.href); if(results == null) { return ""; } else { return decodeURIComponent(results[1].replace(/\+/g, " ")); } } ENV = { RAISE_ON_DEPRECATION: true } App = Em.Application.create({ ready: function() { $("textarea.steppable,input.steppable[type=text]").live('keydown mousewheel', function(e){ if (event.which == 38 || event.which == 40 || event.type == 'mousewheel') { e.preventDefault(); var val = $(this).val(); var cursorIndex = $(this).caret().start; var cursorString = [val.slice(0, cursorIndex), 'CURSOR', val.slice(cursorIndex)].join(''); var re = /(-?\d*)CURSOR\s*(-?\d*)/g var matches = re.exec(cursorString); var number = 1 if(event.altKey) number *= 10 if(event.ctrlKey) number *= 10 if(event.shiftKey) number *= 10 var result = parseInt(matches[1] + matches[2]) + (event.which == 40 || event.wheelDelta < 0 ? -number : number); if(!isNaN(result)){ $(this).val(cursorString.replace(re, result)); } $(this).caret(matches.index, matches.index); } }); $(window).resize(function () { App.graph.resize($(window).height(), $(window).width()) }); $("#charge").live('change', function() { App.graph.set('charge', $(this).val()) }) App.graph.svg = d3.select("#chart").append("svg:svg").attr("width", App.graph.width).attr("height", App.graph.height); this.initialize(); }, Router: Ember.Router.extend({ //enableLogging: true, root: Ember.Route.extend({ index: Ember.Route.extend({ route: '/', connectOutlets: function(router, params) { router.transitionTo('paramsRoute', { lcfCode: lcfCode, animationSpeed: animationSpeed, freezeFrameAt: 0 }); }, deserialize: function(router, params) { // TODO make root route work w/o globals if(getParameterByName('lcfCode') != '') { lcfCode = getParameterByName('lcfCode') animationSpeed = getParameterByName('animationSpeed') var freezeFrameAt = getParameterByName('lockVertices') == 1 ? 1 : 0 } else { lcfCode = '[10]50' animationSpeed = App.animationSpeedsController.objectAt(1).speed var freezeFrameAt = 0 } router.transitionTo('paramsRoute', { lcfCode: lcfCode, animationSpeed: animationSpeed, freezeFrameAt: freezeFrameAt }); App.graph.lcfCode = lcfCode App.graph.animationSpeed = animationSpeed App.graph.freezeFrameAt = freezeFrameAt App.graph.redraw() } }), paramsRoute: Ember.Route.extend({ route: '/:lcfCode/:animationSpeed/:freezeFrameAt', moveElsewhere: Ember.Route.transitionTo('paramsRoute'), deserialize: function(router, params) { App.graph.lcfCode = params.lcfCode App.graph.animationSpeed = params.animationSpeed App.graph.freezeFrameAt = params.freezeFrameAt App.graph.redraw() return params }, serialize: function(router, context) { return { lcfCode: context.lcfCode, animationSpeed: context.animationSpeed, freezeFrameAt: context.freezeFrameAt } }, random: Ember.Route.transitionTo('paramsRoute') }) }) }), ApplicationView: Ember.View.extend({ templateName: 'application', showPermalink: function() { App.controls.set('showPermalink', !App.controls.get('showPermalink')) }, redraw: function() { App.graph.redraw() }, prevFrame: function() { if(App.graph.get('prevFrame') <= 0) return App.graph.set('freezeFrameAt', App.graph.get('prevFrame')) }, nextFrame: function() { // don't use set so graph observer isn't alerted App.graph.freezeFrameAt = App.graph.get('nextFrame') App.get('router').send('moveElsewhere', { lcfCode: App.graph.get('lcfCode'), animationSpeed: App.graph.get('animationSpeed'), freezeFrameAt: App.graph.get('nextFrame') }) App.graph.force.start() App.graph.force.tick() App.graph.force.stop() }, random: function() { function randomBetween(min, max) { return Math.floor(Math.random() * (max - min + 1)) + min } var min_step = -100 var max_step = 100 var num_steps = randomBetween(1, 50) var max_repeats = 25 var arr = [] for (i = 0; i < num_steps; i++) { arr.push(randomBetween(min_step, max_step)) } App.graph.set('lcfCode', '['+arr.join(',')+']'+(randomBetween(1, max_repeats))) }, fullscreen: function() { window.open(window.location.href) } }), ApplicationController: Ember.Controller.extend({ }) }); App.controls = Em.Object.create({ lcfCodeError: null, showPermalink: false, isFullscreen: window.location == window.parent.location, permalink: function() { return window.location.href }.property('App.graph.lcfCode', 'App.graph.animationSpeed', 'App.graph.freezeFrame'), }); App.lcfCodeObject = Ember.Object.extend({ lcfCode: null, init: function() { this._super() this.steps = [] this.edges = [] this.numVertices = 0 this.numRepeats = 0 this.parse() }, parse: function() { this.lcfCode = this.lcfCode.replace(/ /g,"").replace(/−/g,"-") // TODO validate repeats and steps also match repeats var match = this.lcfCode.match(/\[(.*?)\]/) if(match == null) throw "Invalid Syntax" if(match[1] != "") { this.steps = match[1].split(',').filter(Number).map(function(e) {return +e}); } else { throw "Must have at least one step" } this.numRepeats = parseInt(this.lcfCode.split(']')[1]) || 0 if(this.numRepeats < 0) throw "Repeats must be positive" this.numVertices = this.steps.length * (this.numRepeats || 1) var numSteps = this.steps.length var dupCache = {} for(var vertex=0; vertex 0 }.property('freezeFrameAt'), resize: function(height, width){ this.svg.attr('height', height).attr('width', width) this.set('height', height) this.set('width', width) this.set('rcx', width/2 + 110) this.set('rcy', height/2) this.force.size([this.rcx*2, height]); this.redraw() }, prevFrame: function() { return this.get('currentFrame') - 1 }.property('currentFrame'), nextFrame: function() { return this.get('currentFrame') + 1 }.property('currentFrame'), draw: function(lcfCodeObject){ numVertices = lcfCodeObject.numVertices stepsArray = lcfCodeObject.steps var numRepeats = lcfCodeObject.numRepeats var lcfEdges = lcfCodeObject.edges if(numVertices + lcfEdges.length > 50000){ if(!confirm(lcfEdges.length+' edges and '+numVertices+' vertices will be drawn. Continue?')) { return } } this.setProperties({ numVertices: numVertices, numEdges: lcfEdges.length }) var graph = App.graph clearInterval(this.interval) // magic vertex this.nodes = [{fixed: true, x: this.rcx, y: this.rcy}] // arrange nodes in a circle this.nodes = this.nodes.concat(d3.range(numVertices).map(function(d, i) { return { x: App.graph.rcx+App.graph.radius*Math.cos((i*2*Math.PI/numVertices) - Math.PI/2), y: App.graph.rcy+App.graph.radius*Math.sin((i*2*Math.PI/numVertices) - Math.PI/2) } })) this.links = [] this.currentVertex = 0 this.currentVertexOffset = 0 this.force.nodes(this.nodes) this.force.start() if(this.get('animationSpeed') == 0){ this.svg.selectAll(".vertex").style("fill", this.colors[0]) var nodes = this.nodes.slice(1) lcfEdges.forEach(function(edge, i) { graph.links.push({source: nodes[edge.source], target: nodes[edge.target]}); }); this.drawLines() this.force.links(this.links) this.force.start() }else{ // hamiltonian path this.nodes.slice(1).forEach(function(target, i) { App.graph.links.push({source: App.graph.nodes[i == App.graph.nodes.length - 2 ? 1 : i+2], target: target, linkDistance: 0}) }); this.svg.selectAll(".vertex").style("fill", "white") this.drawLines() this.interval = setTimeout(this.animateLcf, this.get('animationSpeed')) } // drawn last so it has the highest z-index this.drawCircles() }, animateLcf: function() { var graph = App.graph var stepInstruction = stepsArray[graph.currentVertex % stepsArray.length] graph.currentStep = (graph.currentVertex + graph.currentVertexOffset) - numVertices * Math.floor((graph.currentVertex + graph.currentVertexOffset) / numVertices) var circles = graph.svg.selectAll(".vertex") if(graph.currentVertexOffset == 0){ circles.filter(function(d,i){return graph.currentVertex == i}).style("fill", graph.colors[2]) circles.filter(function(d,i){return graph.currentVertex > i}).style("fill", graph.colors[0]) circles.filter(function(d,i){return graph.currentVertex < i}).style("fill","white") graph.currentVertexOffset += (stepInstruction > 0 ? 1 : -1); }else if(graph.currentVertexOffset != stepInstruction){ circles.filter(function(d,i){return graph.currentStep == i}).style("fill", graph.colors[1]) graph.currentVertexOffset += (stepInstruction > 0 ? 1 : -1) }else{ circles.filter(function(d,i){return graph.currentStep == i}).style("fill", graph.colors[1]) graph.links.push({source: graph.nodes.slice(1)[graph.currentVertex], target: graph.nodes.slice(1)[graph.currentStep]}); graph.drawLines() graph.currentVertex++ graph.currentVertexOffset = 0; } if(graph.currentVertex != numVertices){ graph.interval = setTimeout(function(){ App.graph.animateLcf() }, App.graph.get('animationSpeed')); } else { graph.interval = null graph.svg.selectAll(".vertex").style("fill", graph.colors[0]) } graph.force.links(graph.links) graph.force.start() }, drawCircles: function() { var circles = this.svg.selectAll("circle") .data(this.nodes) circles.enter() .append("svg:circle") circles.attr("r", function(d, i) { return i == 0 ? 0 : 5 }) .attr("class", function(d, i) { return i == 0 ? 'magic-vertex' : 'vertex' }) .attr("cx", function(d) { return d.x; }) .attr("cy", function(d) { return d.y; }) .call(this.force.drag); circles.exit().remove(); }, drawLines: function(){ var lines = this.svg.selectAll("line.link") .data(this.links) lines.enter() .append("svg:line") lines.attr("class", "link") .attr("x1", function(d) { return d.source.x; }) .attr("y1", function(d) { return d.source.y; }) .attr("x2", function(d) { return d.target.x; }) .attr("y2", function(d) { return d.target.y; }) lines.exit().remove() }, redraw: function() { App.graph.set('currentFrame', 1) try { App.controls.set('lcfCodeError', null) var lcfCodeObject = App.lcfCodeObject.create({ lcfCode: this.get('lcfCode') }); this.draw(lcfCodeObject) } catch(error) { App.controls.set('lcfCodeError', error) } App.get('router').transitionTo('paramsRoute', { lcfCode: this.get('lcfCode'), animationSpeed: this.get('animationSpeed'), freezeFrameAt: this.get('freezeFrameAt') }); }.observes('lcfCode', 'animationSpeed', 'charge', 'freezeFrameAt') }); App.lcfCodesController = Ember.ArrayController.create({ content: Ember.A([ { code: '[2]4', name: 'Tetrahedral graph' }, { code: '[3]6', name: 'Utility graph' }, { code: '[3,-3]4', name: 'Cubical graph' }, { code: '[4]8', name: 'Wagner graph' }, { code: '[6,4,-4]4', name: 'Bidiakis cube' }, { code: '[5,-5]6', name: 'Franklin graph' }, { code: '[-5,-2,-4,2,5,-2,2,5,-2,-5,4,2]', name: 'Frucht graph' }, { code: '[2,6,-2]4', name: 'Truncated tetrahedral graph' }, { code: '[5,-5]7', name: 'Heawood graph' }, { code: '[5,-5]8', name: 'Mobius-Kantor graph' }, { code: '[5,7,-7,7,-7,-5]3', name: 'Pappus graph' }, { code: '[5,-5,9,-9]5', name: 'Desargues graph' }, { code: '[10,7,4,-4,-7,10,-4,7,-7,4]2', name: 'Dodecahedral graph' }, { code: '[12,7,-7]8', name: 'McGee graph' }, { code: '[2,9,-2,2,-9,-2]4', name: 'Truncated cubical graph' }, { code: '[3,-7,7,-3]6', name: 'Truncated octahedral graph' }, { code: '[5,-9,7,-7,9,-5]4', name: 'Nauru graph' }, { code: '[-7,7]13', name: 'F26A graph' }, { code: '[-13,-9,7,-7,9,13]5', name: 'Tutte–Coxeter graph' }, { code: '[5,-5,13,-13]8', name: 'Dyck graph' }, { code: '[-25,7,-7,13,-13,25]9', name: 'Gray graph' }, { code: '[30,-2,2,21,-2,2,12,-2,2,-12,-2,2,-21,-2,2,30,-2,2,-12,-2,2,21,-2,2,-21,-2,2,12,-2,2]2', name: 'Truncated dodecahedral graph' }, { code: '[-29,-19,-13,13,21,-27,27,33,-13,13,19,-21,-33,29]5', name: 'Harries graph' }, { code: '[9,25,31,-17,17,33,9,-29,-15,-9,9,25,-25,29,17,-9,9,-27,35,-9,9,-17,21,27,-29,-9,-25,13,19,-9,-33,-17,19,-31,27,11,-25,29,-33,13,-13,21,-29,-21,25,9,-11,-19,29,9,-27,-19,-13,-35,-9,9,17,25,-9,9,27,-27,-21,15,-9,29,-29,33,-9,-25]', name: 'Harries–Wong graph' }, { code: '[-9,-25,-19,29,13,35,-13,-29,19,25,9,-29,29,17,33,21,9,-13,-31,-9,25,17,9,-31,27,-9,17,-19,-29,27,-17,-9,-29,33,-25,25,-21,17,-17,29,35,-29,17,-17,21,-25,25,-33,29,9,17,-27,29,19,-17,9,-27,31,-9,-17,-25,9,31,13,-9,-21,-33,-17,-29,29]', name: 'Balaban 10-cage' }, { code: '[17,-9,37,-37,9,-17]15', name: 'Foster graph' }, { code: '[16,24,-38,17,34,48,-19,41,-35,47,-20,34,-36,21,14,48,-16,-36,-43,28,-17,21,29,-43,46,-24,28,-38,-14,-50,-45,21,8,27,-21,20,-37,39,-34,-44,-8,38,-21,25,15,-34,18,-28,-41,36,8,-29,-21,-48,-28,-20,-47,14,-8,-15,-27,38,24,-48,-18,25,38,31,-25,24,-46,-14,28,11,21,35,-39,43,36,-38,14,50,43,36,-11,-36,-24,45,8,19,-25,38,20,-24,-14,-21,-8,44,-31,-38,-28,37]', name: 'Biggs-Smith graph' }, { code: '[44,26,-47,-15,35,-39,11,-27,38,-37,43,14,28,51,-29,-16,41,-11,-26,15,22,-51,-35,36,52,-14,-33,-26,-46,52,26,16,43,33,-15,17,-53,23,-42,-35,-28,30,-22,45,-44,16,-38,-16,50,-55,20,28,-17,-43,47,34,-26,-41,11,-36,-23,-16,41,17,-51,26,-33,47,17,-11,-20,-30,21,29,36,-43,-52,10,39,-28,-17,-52,51,26,37,-17,10,-10,-45,-34,17,-26,27,-21,46,53,-10,29,-50,35,15,-47,-29,-41,26,33,55,-17,42,-26,-36,16]', name: 'Balaban 11-cage' }, { code: '[47,-23,-31,39,25,-21,-31,-41,25,15,29,-41,-19,15,-49,33,39,-35,-21,17,-33,49,41,31,-15,-29,41,31,-15,-25,21,31,-51,-25,23,9,-17,51,35,-29,21,-51,-39,33,-9,-51,51,-47,-33,19,51,-21,29,21,-31,-39]2', name: 'Ljubljana graph' }, { code: '[17,27,-13,-59,-35,35,-11,13,-53,53,-27,21,57,11,-21,-57,59,-17]7', name: 'Tutte 12-cage' } ]), selectedChange: function(event) { if(event.selected != null && event.selected != App.graph.get('lcfCode')){ App.graph.set('lcfCode', event.selected) } }.observes('selected') }); App.ApplicationView.lcfCodes = Ember.Select.extend({ contentBinding: "App.lcfCodesController", optionLabelPath: "content.name", optionValuePath: "content.code", valueBinding: "App.lcfCodesController.selected", prompt: " " }) App.ApplicationView.lcfCode = Ember.TextArea.extend({ rows: "5", classNames: ['steppable'], valueBinding: 'App.graph.lcfCode', // TODO respond to mousewheel valueChanged: function(event) { if($.inArray(event.keyCode, [9, 17, 18, 32, 37, 39, 188]) == -1) { App.lcfCodesController.set('selected', event.value) } }.observes('value') }) App.animationSpeedsController = Ember.ArrayController.create({ content: Ember.A([ { speed: '0', name: 'Instant' }, { speed: '1', name: 'Fast' }, { speed: '100', name: 'Slow' } ]), }); App.ApplicationView.animationSpeeds = Ember.Select.extend({ contentBinding: "App.animationSpeedsController", optionLabelPath: "content.name", optionValuePath: "content.speed", valueBinding: "App.graph.animationSpeed", valueChange: function(event) { if(App.graph.get('freezeFrameAt') != 0 && event.selection != null && event.selection != App.animationSpeedsController.objectAt(0)){ $('.freezeFrameCheckbox').trigger('click') } }.observes('value') }) App.ApplicationView.freezeFrame = Ember.Checkbox.extend({ classNames: ['freezeFrameCheckbox'], init: function() { this._super(); Ember.set(this, 'checked', App.graph.get('freezeFrameAt') != 0); }, checkedChange: function(event) { if(event.checked) { App.graph.set('animationSpeed', App.animationSpeedsController.objectAt(0).speed) if(App.graph.get('freezeFrameAt') == 0) { App.graph.set('freezeFrameAt', 1) } } else { App.graph.set('freezeFrameAt', 0) } }.observes('checked') }) })();