diff --git a/kiln/static/js/juration.js b/kiln/static/js/juration.js new file mode 100644 index 0000000..3833a9b --- /dev/null +++ b/kiln/static/js/juration.js @@ -0,0 +1,207 @@ +/* + * juration - a natural language duration parser + * https://github.com/domchristie/juration + * + * Copyright 2011, Dom Christie + * Licenced under the MIT licence + * + */ + +(function() { + + var UNITS = { + seconds: { + patterns: ['second', 'sec', 's'], + value: 1, + formats: { + 'chrono': '', + 'micro': 's', + 'short': 'sec', + 'long': 'second' + } + }, + minutes: { + patterns: ['minute', 'min', 'm(?!s)'], + value: 60, + formats: { + 'chrono': ':', + 'micro': 'm', + 'short': 'min', + 'long': 'minute' + } + }, + hours: { + patterns: ['hour', 'hr', 'h'], + value: 3600, + formats: { + 'chrono': ':', + 'micro': 'h', + 'short': 'hr', + 'long': 'hour' + } + }, + days: { + patterns: ['day', 'dy', 'd'], + value: 86400, + formats: { + 'chrono': ':', + 'micro': 'd', + 'short': 'day', + 'long': 'day' + } + }, + weeks: { + patterns: ['week', 'wk', 'w'], + value: 604800, + formats: { + 'chrono': ':', + 'micro': 'w', + 'short': 'wk', + 'long': 'week' + } + }, + months: { + patterns: ['month', 'mon', 'mo', 'mth'], + value: 2628000, + formats: { + 'chrono': ':', + 'micro': 'm', + 'short': 'mth', + 'long': 'month' + } + }, + years: { + patterns: ['year', 'yr', 'y'], + value: 31536000, + formats: { + 'chrono': ':', + 'micro': 'y', + 'short': 'yr', + 'long': 'year' + } + } + }; + + var stringify = function(seconds, options) { + + if(!_isNumeric(seconds)) { + throw "juration.stringify(): Unable to stringify a non-numeric value"; + } + + if((typeof options === 'object' && options.format !== undefined) && (options.format !== 'micro' && options.format !== 'short' && options.format !== 'long' && options.format !== 'chrono')) { + throw "juration.stringify(): format cannot be '" + options.format + "', and must be either 'micro', 'short', or 'long'"; + } + + var defaults = { + format: 'short' + }; + + var opts = _extend(defaults, options); + + var units = ['years', 'months', 'days', 'hours', 'minutes', 'seconds'], values = []; + var remaining = seconds; + for(var i = 0, len = units.length; i < len; i++) { + var unit = UNITS[units[i]]; + values[i] = Math.floor(remaining / unit.value); + + if(opts.format === 'micro' || opts.format === 'chrono') { + values[i] += unit.formats[opts.format]; + } + else { + values[i] += ' ' + _pluralize(values[i], unit.formats[opts.format]); + } + remaining = remaining % unit.value; + } + var output = ''; + for(i = 0, len = values.length; i < len; i++) { + if(values[i].charAt(0) !== "0" && opts.format != 'chrono') { + output += values[i] + ' '; + } + else if (opts.format == 'chrono') { + output += _padLeft(values[i]+'', '0', i==values.length-1 ? 2 : 3); + } + } + return output.replace(/\s+$/, '').replace(/^(00:)+/g, '').replace(/^0/, ''); + }; + + var parse = function(string) { + + // returns calculated values separated by spaces + for(var unit in UNITS) { + for(var i = 0, mLen = UNITS[unit].patterns.length; i < mLen; i++) { + var regex = new RegExp("((?:\\d+\\.\\d+)|\\d+)\\s?(" + UNITS[unit].patterns[i] + "s?(?=\\s|\\d|\\b))", 'gi'); + string = string.replace(regex, function(str, p1, p2) { + return " " + (p1 * UNITS[unit].value).toString() + " "; + }); + } + } + + var sum = 0, + numbers = string + .replace(/(?!\.)\W+/g, ' ') // replaces non-word chars (excluding '.') with whitespace + .replace(/^\s+|\s+$|(?:and|plus|with)\s?/g, '') // trim L/R whitespace, replace known join words with '' + .split(' '); + + for(var j = 0, nLen = numbers.length; j < nLen; j++) { + if(numbers[j] && isFinite(numbers[j])) { + sum += parseFloat(numbers[j]); + } else if(!numbers[j]) { + throw "juration.parse(): Unable to parse: a falsey value"; + } else { + // throw an exception if it's not a valid word/unit + throw "juration.parse(): Unable to parse: " + numbers[j].replace(/^\d+/g, ''); + } + } + return sum; + }; + + // _padLeft('5', '0', 2); // 05 + var _padLeft = function(s, c, n) { + if (! s || ! c || s.length >= n) { + return s; + } + + var max = (n - s.length)/c.length; + for (var i = 0; i < max; i++) { + s = c + s; + } + + return s; + }; + + var _pluralize = function(count, singular) { + return count == 1 ? singular : singular + "s"; + }; + + var _isNumeric = function(n) { + return !isNaN(parseFloat(n)) && isFinite(n); + }; + + var _extend = function(obj, extObj) { + for (var i in extObj) { + if(extObj[i] !== undefined) { + obj[i] = extObj[i]; + } + } + return obj; + }; + + var juration = { + parse: parse, + stringify: stringify, + humanize: stringify + }; + + if ( typeof module === "object" && module && typeof module.exports === "object" ) { + //loaders that implement the Node module pattern (including browserify) + module.exports = juration; + } else { + // Otherwise expose juration + window.juration = juration; + + // Register as a named AMD module + if ( typeof define === "function" && define.amd ) { + define("juration", [], function () { return juration; } ); + } + } +})(); \ No newline at end of file diff --git a/kiln/static/js/temp_graph.js b/kiln/static/js/temp_graph.js index 5caa048..7399729 100644 --- a/kiln/static/js/temp_graph.js +++ b/kiln/static/js/temp_graph.js @@ -84,11 +84,11 @@ var tempgraph = (function(module) { if (marker !== undefined && marker) { var selector = className.replace(" ", "."); - var marker = this.axes.append("g") - .selectAll("."+selector+".dot").data(data) - .enter().append("circle") + var key = data.id === undefined ? undefined : function(d){ return d.id;}; + var marker = this.axes.append("g").selectAll("."+selector+".dot") + .data(data, key).enter().append("circle") .attr("class", className+" dot") - .attr("r", 5) + .attr("r", 10) .attr("cx", function(d) { return this.x(d.x); }.bind(this)) .attr("cy", function(d) { return this.y(d.y); }.bind(this)); } @@ -154,7 +154,21 @@ var tempgraph = (function(module) { this.lines[className].data = data; this.axes.select("path."+className).datum(data) .attr("d", this.lines[className].line); + + var join, selector; + if (this.lines[className].marker) { + selector = className.replace(" ", "."); + join = this.axes.selectAll("."+selector+".dot") + .data(data, function(d){ return d.id;}); + join.enter().append("circle") + .attr("class", className+" dot") + .attr("r", 10); + join.exit().remove(); + join.attr("cx", function(d) { return this.x(d.x); }.bind(this)) + .attr("cy", function(d) { return this.y(d.y); }.bind(this)); + } this.draw(); + return join; } module.Graph.prototype.xlim = function(min, max) { if (min === undefined) diff --git a/kiln/static/js/temp_monitor.js b/kiln/static/js/temp_monitor.js index b89f64c..819c7a7 100644 --- a/kiln/static/js/temp_monitor.js +++ b/kiln/static/js/temp_monitor.js @@ -191,6 +191,8 @@ var tempgraph = (function(module) { this.scale = function(temp) { return temp * 9 / 5 + 32; } this.inverse = function(temp) { return (temp - 32) * 5 / 9;} this.print = function(t) { return t+"°F"} + } else if (name == "cone") { + } } module.TempScale.C_to_cone = function(temp) { diff --git a/kiln/static/js/temp_profile.js b/kiln/static/js/temp_profile.js index f06c73c..c982be9 100644 --- a/kiln/static/js/temp_profile.js +++ b/kiln/static/js/temp_profile.js @@ -14,7 +14,31 @@ var tempgraph = (function(module) { this.graph = graph; this.scalefunc = scale; - this.setupGraph(); + + //immediately view range from 10 min before to end time of profile + var now = new Date(); + var rstart = new Date(now.getTime() - 10*60*100); + var rend = this.time_finish(now); + this.graph.xlim(rstart, rend); + + this.pane = this.graph.pane.insert("rect", ":first-child") + .attr("class", "profile-pane") + .attr("height", this.graph.height) + .attr("clip-path", "url(#pane)") + + this.line = this.graph.plot(this._schedule(), "profile-line", true); + + this.drag = d3.behavior.drag().origin(function(d) { + return {x:this.graph.x(d.x), y:this.graph.y(d.y)}; + }.bind(this)).on("dragstart", function(d) { + d3.event.sourceEvent.stopPropagation(); + this._node = this._findNode(d); + }.bind(this)).on("drag", this.dragNode.bind(this)); + + this.update(); + + //events + this._bindUI(); } module.Profile.prototype.time_finish = function(now) { if (this.time_start instanceof Date) { @@ -27,50 +51,6 @@ var tempgraph = (function(module) { this.scalefunc = scale; this.update(); } - module.Profile.prototype.setupGraph = function() { - //immediately view range from 10 min before to end time of profile - var now = new Date(); - var rstart = new Date(now.getTime() - 10*60*100); - var rend = this.time_finish(now); - this.graph.xlim(rstart, rend); - - this.pane = this.graph.pane.insert("rect", ":first-child") - .attr("class", "profile-pane") - .attr("height", this.graph.height) - - this.line = this.graph.plot(this._schedule(), "profile-line", true); - this.update(); - - //events - this.drag = d3.behavior.drag().origin(function(d) { - return {x:this.graph.x(d.x), y:this.graph.y(d.y)}; - }.bind(this)).on("dragstart", function(d) { - d3.event.sourceEvent.stopPropagation(); - this._node = this._findNode(d); - }.bind(this)).on("drag", this.dragNode.bind(this)); - this.line.marker.call(this.drag); - - var hide_info = function() { - this.hide_timeout = setTimeout(function() { $("#profile-node-info").hide(); }, 250); - }; - this.graph.zoom.on("zoom.profile", this.update.bind(this)); - this.line.marker.on("mouseover", this.hoverNode.bind(this)); - this.line.marker.on("mouseout", hide_info.bind(this)); - $("#profile-node-info").on("mouseout.profile", hide_info.bind(this)); - $("#profile-node-info").on("mouseover.profile", function() { - clearTimeout(this.hide_timeout); - }.bind(this)); - } - module.Profile.prototype._schedule = function() { - var start_time = this.time_start instanceof Date ? this.time_start : new Date(); - var schedule = []; - for (var i = 0; i < this.schedule.length; i++) { - var time = new Date(start_time.getTime() + this.schedule[i][0]*1000); - var temp = this.scalefunc.scale(this.schedule[i][1]); - schedule.push({x:time, y:temp}); - } - return schedule; - } module.Profile.prototype.update = function() { var start_time = this.time_start instanceof Date ? this.time_start : new Date(); var end_time = new Date(start_time.getTime()+this.length*1000); @@ -78,49 +58,164 @@ var tempgraph = (function(module) { this.pane.attr("width", width) .attr("transform","translate("+this.graph.x(start_time)+",0)"); - this.graph.update("profile-line", this._schedule()); + var join = this.graph.update("profile-line", this._schedule()); + join.on("mouseover.profile", this.hoverNode.bind(this)) + .on("mouseout.profile", this._hideInfo.bind(this)) + .on("dblclick.profile", this.delNode.bind(this)); + join.call(this.drag); } - module.Profile.prototype.setScale = function(scale) { - this.scalefunc = scale; - this.update(); + + module.Profile.prototype._bindUI = function() { + // Info pane events + var updateNode = function() { + clearTimeout(this.timeout_infoedit); + var time = juration.parse($("#profile-node-info input.time").val()); + var temp = parseFloat($("#profile-node-info input.temp").val()); + this._updateNode(this._node, time, temp); + }.bind(this) + + $("#profile-node-info").on("mouseout.profile", this._hideInfo.bind(this)); + $("#profile-node-info").on("mouseover.profile", function() { + clearTimeout(this.timeout_infohide); + }.bind(this)); + $("#profile-node-info input").on("keypress", function(e) { + clearTimeout(this.timeout_infoedit); + if (e.keyCode == 13) { + updateNode(); + } else { + this.timeout_infoedit = setTimeout(updateNode, 2000); + } + }.bind(this)); + $("#profile-node-info input").on("blur", function() { + this._focused = false; + updateNode(); + this._hideInfo(); + }.bind(this)); + $("#profile-node-info input").on("focus", function() { + this._focused = true; + }.bind(this)); + + //Graph events + this.graph.zoom.on("zoom.profile", this.update.bind(this)); + this.line.marker.on("dblclick", this.delNode.bind(this)); + this.graph.pane.on("dblclick", this.addNode.bind(this)); + } + module.Profile.prototype._schedule = function() { + var start_time = this.time_start instanceof Date ? this.time_start : new Date(); + var schedule = []; + for (var i = 0; i < this.schedule.length; i++) { + var time = new Date(start_time.getTime() + this.schedule[i][0]*1000); + var temp = this.scalefunc.scale(this.schedule[i][1]); + schedule.push({id:i, x:time, y:temp}); + } + return schedule; + } + module.Profile.prototype._hideInfo = function() { + this.timeout_infohide = setTimeout(function() { + if (!this._focused) + $("#profile-node-info").fadeOut(100); + }.bind(this), 250); } module.Profile.prototype._findNode = function(d) { - var time, temp, - start_time = this.time_start instanceof Date ? this.time_start : new Date(); - for (var i = 0; i < this.schedule.length; i++) { - time = new Date((start_time.getTime() + this.schedule[i][0]*1000)); - temp = this.schedule[i][1]; - //if time is within 10 seconds and temperature matches exactly - if ((time - d.x) < 10000 && d.y == this.scalefunc.scale(temp)) - return i; + return d.id; + } + module.Profile.prototype._updateNode = function(node, time, temp) { + var start_time = this.time_start instanceof Date ? this.time_start : new Date(); + if (!(time instanceof Date)) { + //This is probably just a direct offset, no need to compute time + this.schedule[node][0] = time; + } else if (node == 0) { + this.schedule[node][0] = 0; + } else { + var newtime = (time - start_time.getTime()) / 1000; + this.schedule[node][0] = newtime; } + //update length only if we're editing the final node + if (node == this.schedule.length-1) { + this.length = this.schedule[node][0]; + } + this.schedule[node][1] = this.scalefunc.inverse(temp); + + //if we're dragging this node "behind" the previous, push it back as well + //except if the previous one is the first node, in which case just set it to zero + if (node > 0 && this.schedule[node-1][0] >= newtime) { + if (node-1 == 0) + this.schedule[node][0] = 0; + else + this.schedule[node-1][0] = newtime; + } else if (node < this.schedule.length-1 && this.schedule[node+1][0] < newtime){ + this.schedule[node+1][0] = newtime; + if (node+1 == this.schedule.length-1) + this.length = this.schedule[node+1][0]; + } + this._showInfo(node); + this.update(); + + //Unlock the save buttons and names + + } + module.Profile.prototype._showInfo = function(node) { + this._node = node; + var start_time = this.time_start instanceof Date ? this.time_start : new Date(); + var time = new Date(this.schedule[node][0]*1000 + start_time.getTime()); + var temp = this.scalefunc.scale(this.schedule[node][1]); + $("#profile-node-info") + .css('left', this.graph.x(time)+80) + .css('top', this.graph.y(temp)+50) + .fadeIn(100); + + $("#profile-node-info div.name").text("Set point "+(node+1)); + $("#profile-node-info input.temp").val(this.scalefunc.print(Math.round(temp*100)/100)); + var timestr; + try { + timestr = juration.stringify(this.schedule[node][0]); + } catch (e) { + timestr = 0; + } + $("#profile-node-info input.time").val(timestr); } module.Profile.prototype.addNode = function() { + d3.event.stopPropagation(); + var mouse = d3.mouse(this.graph.pane[0][0]); + var start_time = this.time_start instanceof Date ? this.time_start : new Date(); + var secs = (this.graph.x.invert(mouse[0]) - start_time) / 1000; + + var start, end; + for (var i = 0; i < this.schedule.length-1; i++) { + start = this.schedule[i][0]; + end = this.schedule[i+1][0]; + if (start < secs && secs < end) { + var t2 = this.schedule[i+1][1], t1 = this.schedule[i][1]; + var frac = (secs - start) / (end - start); + var temp = frac * (t2 - t1) + t1; + this.schedule.splice(i+1, 0, [secs, temp]); + } + } + this.update(); } - module.Profile.prototype.delNode = function() { - + module.Profile.prototype.delNode = function(d) { + d3.event.stopPropagation(); + var node = this._findNode(d); + //ignore attempts to delete the starting and ending nodes + if (node != 0 && this.schedule.length > 2) { + this.schedule.splice(node, 1); + if (node == this.schedule.length) { + this.length = this.schedule[node-1][0]; + } + } + this.update(); } module.Profile.prototype.dragNode = function(d) { var time = this.graph.x.invert(d3.event.x); var temp = this.graph.y.invert(d3.event.y); - var start_time = this.time_start instanceof Date ? this.time_start : new Date(); - this.schedule[this._node][0] = (time - start_time) / 1000; - this.schedule[this._node][1] = this.scalefunc.inverse(temp); - this.update(); + this._updateNode(d.id, time, temp); } module.Profile.prototype.hoverNode = function(d) { - clearTimeout(this.hide_timeout); - var node = this._findNode(d); - $("#profile-node-info") - .css('left', this.graph.x(d.x)+80) - .css('top', this.graph.y(d.y)+50) - .show(); - - $("#profile-node-info div.name").text("Set point "+(node+1)); - $("#profile-node-info input.temp").val(this.scalefunc.scale(this.schedule[node][1])); - $("#profile-node-info input.time"); + clearTimeout(this.timeout_infohide); + this._showInfo(d.id); } + return module; }(tempgraph || {})); diff --git a/kiln/templates/main.html b/kiln/templates/main.html index 1b94660..5254381 100644 --- a/kiln/templates/main.html +++ b/kiln/templates/main.html @@ -130,6 +130,7 @@ +