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 @@
+