Initial version.
authorMark Wooding <mdw@distorted.org.uk>
Sat, 19 Jan 2013 01:31:47 +0000 (01:31 +0000)
committerMark Wooding <mdw@distorted.org.uk>
Sat, 19 Jan 2013 01:31:47 +0000 (01:31 +0000)
dep-ui.css [new file with mode: 0644]
dep-ui.js [new file with mode: 0644]
dep.js [new file with mode: 0644]
rolling.css [new file with mode: 0644]
rolling.html [new file with mode: 0644]

diff --git a/dep-ui.css b/dep-ui.css
new file mode 100644 (file)
index 0000000..7e191c4
--- /dev/null
@@ -0,0 +1,8 @@
+input.bad { background-color: #ff6666; }
+input[readonly] {
+       border: 1px inset;
+       background-color: #eeeeee;
+}
+input.bad[readonly] {
+       background-color: #dd5555;
+}
diff --git a/dep-ui.js b/dep-ui.js
new file mode 100644 (file)
index 0000000..29f5d6b
--- /dev/null
+++ b/dep-ui.js
@@ -0,0 +1,206 @@
+/* -*-javascript-*-
+ *
+ * Dependency-based user interface in a web page.
+ *
+ * (c) 2013 Mark Wooding
+ */
+
+/*----- Licensing notice --------------------------------------------------*
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License as
+ * published by the Free Software Foundation; either version 2 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU Library General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, see <http://www.gnu.org/licenses/>.
+ */
+
+var DEP_UI = {}; (function () { with (DEP_UI) {
+
+/*----- Utility functions and classes -------------------------------------*/
+
+DEP_UI.elt = function (id) {
+  /* Find and return the element with the given ID. */
+  return document.getElementById(id);
+}
+
+DEP_UI.add_elt_class = function (elt, cls) {
+  /* Add the class name CLS to element ELT's `class' attribute. */
+
+  if (!elt.className.match('\\b' + cls + '\\b'))
+    elt.className += ' ' + cls
+}
+
+DEP_UI.rm_elt_class = function (elt, cls) {
+  /* Remove the class name CLS from element ELT's `class' attribute. */
+
+  elt.className = elt.className.replace(
+    new RegExp ('\\s*\\b' + cls + '\\b\\s*'), ' ');
+}
+
+/* A gadget which can arrange to perform an idempotent action (the FUNC
+ * argument) again `soon'.
+ */
+DEP_UI.Soon = function (func) {
+  this.timer = null;
+  this.func = func;
+}
+Soon.prototype = {
+  kick: function () {
+    /* Make sure the function is called again soon.  If we've already been
+     * kicked, then put off the action for a bit more (in case things need
+     * time to settle).
+     */
+
+    var me = this;
+    if (this.timer !== null) clearTimeout(this.timer);
+    this.timer = setTimeout(function () { me.func(); }, 50);
+  }
+};
+
+/*----- Conversion machinery ----------------------------------------------*/
+
+/* An exception, thrown if a conversion function doesn't like what it
+ * sees.
+ */
+DEP_UI.BadValue = new DEP.Tag('BadValue');
+
+DEP_UI.convert_to_numeric = function (string) {
+  /* Convert the argument STRING to a number. */
+
+  if (!string.match('\\S')) throw BadValue;
+  var n = Number(string);
+  if (n !== n) throw BadValue;
+  return n;
+}
+
+DEP_UI.convert_from_numeric = function (num) {
+  /* Convert the argument number NUM to a string, in a useful way. */
+  return num.toFixed(3);
+}
+
+/*----- User interface functions ------------------------------------------*/
+
+/* A list of input fields which might need periodic kicking. */
+var KICK_INPUT_FIELDS = [];
+
+DEP_UI.input_field = function (id, dep, convert) {
+  /* Bind an input field (with the given ID) to a DEP, converting the user's
+   * input with the CONVERT function.
+   */
+
+  var e = elt(id);
+
+  function kick() {
+    /* Update the dep from the element content.  If the convert function
+     * doesn't like the input then mark the dep as bad and highlight the
+     * input element.
+     */
+
+    var val, err;
+
+    try {
+      val = convert(e.value);
+      if (!dep.goodp())
+       rm_elt_class(e, 'bad');
+      dep.set_value(val);
+    } catch (err) {
+      if (err !== BadValue) throw err;
+      dep.make_bad();
+      add_elt_class(e, 'bad');
+    }
+  }
+
+  // Name the dep after our id.
+  dep.name = id;
+
+  // Arrange to update the dep `shortly after' updates.
+  var soon = new Soon(kick);
+  function kick_soon () { soon.kick(); }
+  e.addEventListener('click', kick_soon);
+  e.addEventListener('blur', kick_soon);
+  e.addEventListener('keypress', kick_soon);
+
+  // Sadly, the collection of events above isn't comprehensive, because we
+  // don't actually get told about edits as a result of clipboard operations,
+  // or even (sometimes) deletes, so add our `kick' function to a list of
+  // such functions to be run periodically just in case.
+  KICK_INPUT_FIELDS.push(kick);
+}
+
+DEP_UI.input_radio = function (id, dep) {
+  /* Bind a radio button (with the given ID) to a DEP.  When the user frobs
+   * the button, set the dep to the element's `value' attribute.
+   */
+
+  var e = elt(id);
+
+  function kick () {
+    // Make sure we're actually chosen.  We get called periodically
+    // regardless of user input.
+    if (e.checked) dep.set_value(e.value);
+  };
+
+  // Name the dep after our id.
+  dep.name = id;
+
+  // Arrange to update the dep `shortly after' updates.
+  var soon = new Soon(kick);
+  function kick_soon () { soon.kick(); }
+  e.addEventListener('click', kick_soon);
+  e.addEventListener('changed', kick_soon);
+
+  // The situation for radio buttons doesn't seem as bad as for text widgets,
+  // but let's be on the safe side.
+  KICK_INPUT_FIELDS.push(kick);
+}
+
+DEP_UI.output_field = function (id, dep, convert) {
+  /* Bind a DEP to an output element (given by ID), converting the dep's
+   * value using the CONVERT function.
+   */
+
+  var e = elt(id);
+
+  function kicked() {
+    /* Update the element, highlighting it if the dep is bad. */
+    if (dep.goodp()) {
+      rm_elt_class(e, 'bad');
+      e.value = convert(dep.value());
+    } else {
+      add_elt_class(e, 'bad');
+      e.value = '';
+    }
+  }
+
+  // Name the dep after our id.
+  dep.name = id;
+
+  // Keep track of the dep's value.
+  dep.add_listener(kicked);
+  kicked();
+}
+
+/*----- Periodic maintenance ----------------------------------------------*/
+
+function kick_all() {
+  /* Kick all of the input fields we know about.  Their `kick' functions are
+   * all on the list `KICK_INPUT_FIELDS'.
+   */
+  DEP.dolist(KICK_INPUT_FIELDS, function (func) { func(); });
+}
+
+// Update the input fields relatively frequently.
+setInterval(kick_all, 500);
+
+// And make sure we get everything started when the page is fully loaded.
+window.addEventListener('load', kick_all);
+
+/*----- That's all, folks -------------------------------------------------*/
+} })();
diff --git a/dep.js b/dep.js
new file mode 100644 (file)
index 0000000..4376b89
--- /dev/null
+++ b/dep.js
@@ -0,0 +1,512 @@
+/* -*-javascript-*-
+ *
+ * Dependency-based computation.
+ *
+ * (c) 2013 Mark Wooding
+ */
+
+/*----- Licensing notice --------------------------------------------------*
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License as
+ * published by the Free Software Foundation; either version 2 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU Library General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, see <http://www.gnu.org/licenses/>.
+ */
+
+var DEP = { }; (function () { with (DEP) {
+
+/*----- Utility functions and classes -------------------------------------*/
+
+DEP.dolist = function (list, func) {
+  /* Apply FUNC to each element of LIST, discarding the results.
+   *
+   * JavaScript's looping iterates over indices rather than values, which is
+   * consistent with what you want from a mapping, but inconvenient for
+   * sequences.
+   */
+
+  var i;
+  for (i in list) func(list[i]);
+}
+
+function eql(x, y) {
+  /* A standard equality function, suitable for detecting changes to `Dep'
+   * objects.  This may be needlessly sensitive for some cases, but at least
+   * that's slow rather than wrong.
+   */
+
+  return x === y;
+}
+
+/* A Tag is an object which is interesting only because of its identity, and
+ * that the set of Tags is basically determined by the static structure of
+ * the program.
+ */
+DEP.Tag = function (what) {
+  this.what = what;
+}
+Tag.prototype = {
+  toString: function () { return '#{Tag ' + this.what + '}'; }
+}
+
+/* A Generation is like a Tag, except that it's expected that a program will
+ * manufacture Generations repeatedly, and perhaps use them to determine
+ * whether an object is `up-to-date' in some sense.
+ */
+DEP.Generation = function (what) {
+  this.what = what;
+  this.seq = Generation._next++;
+}
+Generation.prototype = {
+  toString: function () {
+    return '#{Generation ' + this.what + ' #' + this.seq.toString() + '}';
+  }
+};
+Generation._next = 0;
+DEP.Generation = Generation;
+
+/*----- The system state --------------------------------------------------*/
+
+var GENERATION = new Generation('dep-generation');
+                                       // Current recomputation generation.
+
+var EVALUATING = null;                 // The dep being re-evaluated.
+var STATE = 'ready';                   // The current state.
+
+/* Flags for Dep objects. */
+var F_VALUE = 1;                       // Value is up-to-date.
+var F_DEPS = 2;                                // Known as a dependent.
+var F_CHANGED = 4;                     // Changed in current phase.
+var F_RECOMPUTING = 8;                 // Currently being recomputed.
+var F_QUEUED = 16;                     // Queued for recomputation.
+
+var BAD = Tag('BAD')                   // Used for the value of `bad' deps.
+
+var DELAYED = [];                      // Actions delayed by `with_frozen'.
+var PENDING = [];                      // Deps awaiting recomputation.
+
+/*----- Exceptions --------------------------------------------------------*/
+
+DEP.CircularDependency = new Tag('CircularDependency');
+DEP.BusyRecomputing = new Tag('BusyRecomputing');
+
+/*----- Main code ---------------------------------------------------------*/
+
+/* A `Dep' object maintains a value, either because it was provided
+ * explicitly by the programmer, or computed in terms of other `Dep' objects.
+ * In the latter case, its value will be recomputed if any of its
+ * dependencies change value.
+ *
+ * A `Dep' object may have listener functions attached to it.  These
+ * functions will bw called when its value changes.
+ *
+ * A `Dep' may be `bad'.  In this case, use of its value by a dependent
+ * causes that `Dep' to become bad in turn.
+ */
+
+DEP.Dep = function (value, maybefunc) {
+  /* Initialize a new `Dep' object.
+   *
+   * Handling of the arguments is a little fiddly.  If two arguments are
+   * provided then the first is set as the value and the second as the
+   * recomputation function.  Otherwise, the first argument is interpreted as
+   * the recomputation function if it has `function' type, or as an initial
+   * explicit value otherwise.  To force a function to be interpreted as an
+   * initial value, pass a second argument of `null'.
+   *
+   * Some properties can be fiddled with by client programs (rather than
+   * trying to invent some crazy option-parsing protocol in the constructor).
+   *
+   * `name'            A string name for this `Dep', used when printing.
+   *
+   * `equivp'          A predicate for deciding whether two value assigned
+   *                   to this `Dep' are equivalent.  Used to decide whether
+   *                   a change should be propagated.
+   */
+
+  var val, func, f = F_CHANGED;
+  var me = this;
+
+  // Work out what's going on with our crazy argument convention.
+  if (maybefunc !== undefined) {
+    val = value;
+    func = maybefunc;
+    f |= F_VALUE | F_QUEUED;
+  } else if (typeof value === 'function') {
+    val = BAD;
+    func = value;
+    f |= F_QUEUED;
+  } else {
+    val = value;
+    func = null;
+    f |= F_VALUE | F_DEPS;
+  }
+
+  // Initialize the various slots.
+  this._value_function = func;         // Recomputation function.
+  this._value = val;                   // Current value.
+
+  this.name = undefined;               // Name, for printing.
+  this.equivp = eql;                   // Equivalent-value predicate.
+  this.__flags = f;                    // Current flags.
+  this._generation = GENERATION;       // Have we done this one already?.
+  this._listeners = [];                        // List of listener functions.
+
+  // We need to maintain the dependency graph.  In order to prevent duplicate
+  // entries in the various sets, we use maps rather than lists, but this
+  // presents a problem with coming up with keys: JavaScript stringifies
+  // objects rather than using them as-is, which is obviously hopeless.  So
+  // assign each `Dep' a unique sequence number and use that as the key.
+  this._seq = Dep._seq++;              // For identifying this object.
+  this._dependents = { };              // Set of dependent objects.
+  this._dependencies = { };            // Set of objects we depend on.
+
+  // If we have a recomputation function then exercise it.
+  if (func !== null) {
+    with_frozen(function () {
+      PENDING.push(me);
+    });
+  }
+}
+
+Dep._seq = 0;                          // Next sequence number to allocate.
+
+Dep.prototype = {
+
+  toString: function () {
+    /* Convert the receiver to a string.
+     *
+     * The printed representation includes a number of bits of information
+     * which are probably meaningless to most readers but are handy for
+     * debugging this library if it's misbehaving.
+     */
+
+    // Basic stuff.
+    var s = '#{Dep';
+    var f = this._flags();
+    var v = this._value;
+
+    // The dep's name.
+    if (this.name !== null) s += ' "' + this.name + '"';
+
+    // Sequence number -- distinguishes the dep if nothing else will.
+    s += ' #' + this._seq;
+
+    // The value, or some kind of marker that it doesn't have one.
+    if (!(f & F_VALUE)) s += ' #{out-of-date}';
+    else if (v === BAD) s += ' #{bad}';
+    else s += ' ' + v.toString();
+
+    // Various flags.
+    if (!(f & F_DEPS)) s += ' :recompute-deps';
+    if (f & F_QUEUED) s += ' :queued';
+    if (f & F_CHANGED) s += ' :changed';
+
+    // Done.
+    s += '}';
+    return s;
+  },
+
+  _flags: function () {
+    /* Return the receiver's flags.
+     *
+     * If the receiver isn't up-to-date then we synthesize the flags.
+     */
+
+    if (this.state === 'ready') return F_VALUE | F_DEPS;
+    else if (this._generation === GENERATION) return this.__flags;
+    else if (this._value_function === null) return F_VALUE | F_DEPS;
+    else return 0;
+  },
+
+  _update: function (value) {
+    /* Store VALUE as the receiver's value.
+     *
+     * Return whether the value has changed as a result.  This is a low-level
+     * function which doesn't handle propagation or anything like that.
+     */
+
+    if (value === BAD ?
+       this._value === BAD :
+       (this._value !== BAD &&
+        this.equivp(value, this._value)))
+      return false;
+    else {
+      this._value = value;
+      return true;
+    }
+  },
+
+  _new_value: function () {
+    /* Run the `_value_function' of the receiver, returning the result.
+     *
+     * If a bad dep is read during this then return `BAD'.  Other exceptions
+     * are propagated in the usual way.
+     */
+
+    var old = EVALUATING;
+    var val, e;
+
+    this._dependencies = { };
+    try {
+      EVALUATING = this;
+      try {
+       return this._value_function();
+      } catch (e) {
+       if (e === BAD) return BAD;
+       else throw e;
+      }
+    } finally {
+      EVALUATING = old;
+    }
+  },
+
+  _propagate: function () {
+    /* Propagate a change in the receiver to dependents and listeners. */
+
+    var d, di, f;
+
+    // Iterate over each dependent, pushing each one onto the `PENDING'
+    // queue, bringing it into the current generation, and marking it as
+    // having no current value.
+    for (di in this._dependents) {
+      d = this._dependents[di];
+      f = d._flags();
+      if (!(f & (F_QUEUED | F_DEPS))) {
+       PENDING.push(d);
+       d._generation = GENERATION;
+       d.__flags = (f & ~F_VALUE) | F_QUEUED;
+      }
+    }
+
+    // We no longer have any dependents.  Maybe we'll acquire more when the
+    // old dependents recompute themselves.
+    this._dependents = { };
+
+    // Tell the listeners that something interesting happened.
+    dolist(this._listeners, function (l) { l(); });
+  },
+
+  _recompute: function () {
+    /* Recompute the receiver, propagating changes and so on.
+     *
+     * Return whether we actually needed to change anything.
+     */
+
+    var queued = this.__flags & F_QUEUED;
+    var e;
+    var me = this;
+
+    // Update us with the given VALUE.
+    function update(value) {
+      if (me._update(value)) {
+       me.__flags = queued | F_VALUE | F_DEPS | F_CHANGED;
+       me._propagate();
+       return true;
+      } else {
+       me.__flags = queued | F_VALUE | F_DEPS;
+       return false;
+      }
+    };
+
+    // Try to recompute our value.  If that doesn't work then mark us as bad
+    // and propagate that.
+    try {
+      return update(this._new_value());
+    } catch (e) {
+      update(BAD);
+      throw e;
+    }
+  },
+
+  _force: function () {
+    /* Make sure the receiver has a current value.
+     *
+     * Return true if the receiver's value has changed in this recomputation
+     * phase.
+     *
+     * If it already has a good value then just return.  Otherwise mark it
+     * as recomputing (to trap circularities) and poke our dependencies to
+     * ensure that they're up-to-date.  If they weren't, then we recompute
+     * our own value before returning.
+     */
+
+    var f = this._flags();
+    var d, any = false;
+
+    if (f & F_RECOMPUTING) throw CircularDependency;
+    else if (f & F_VALUE) return f & F_CHANGED;
+    else {
+      this._generation = GENERATION;
+      this.__flags = (f & ~F_QUEUED) | F_RECOMPUTING;
+      for (d in this._dependencies)
+       if (this._dependencies[d]._force()) any = true;
+      if (any)
+       return this._recompute();
+      else {
+       this.__flags = f;
+       return false;
+      }
+    }
+  },
+
+  value: function () {
+    /* Fetch and return the receiver's current value.
+     *
+     * If the receiver is bad then an exception is thrown.  This exception
+     * can be caught using `orelse'; a dep recomputation function can let the
+     * exception propagate, and be marked as bad in turn.
+     */
+
+    var val;
+
+    if (state === 'recomputing') {
+      if (EVALUATING) {
+       this._dependents[EVALUATING._seq] = EVALUATING;
+       EVALUATING._dependencies[this._seq] = this;
+      }
+      this._force();
+    }
+    val = this._value;
+    if (val === BAD) throw BAD;
+    return val;
+  },
+
+  set_value: function (value) {
+    /* Set the receiver's value to VALUE, and propagate. */
+
+    var me = this;
+
+    with_frozen(function () {
+      if (me._update(value)) {
+       me._generation = GENERATION;
+       me.__flags = F_VALUE | F_CHANGED;
+       me._propagate();
+      }
+    });
+  },
+
+  make_bad: function () {
+    /* Mark the receiver as being bad, and propagate. */
+    this.set_value(BAD);
+  },
+
+  add_listener: function (func) {
+    /* Add FUNC to the receiver's list of listeners.
+     *
+     * Listener functions are called without arguments, and any values
+     * returned are ignored.
+     */
+
+    this._listeners.push(func);
+  },
+
+  goodp: function () {
+    /* Return whether the receiver is good (i.e., not marked as bad). */
+
+    return this._value !== BAD;
+  }
+};
+
+DEP.orelse = function (thunk, errthunk) {
+  /* Call THUNK.  If it succeeds, then return its result.  If THUNK
+   * reads a bad dep then call ERRTHINK and return its result instead.
+   */
+
+  var e;
+  try { return thunk(); }
+  catch (e) {
+    if (e === BAD) { return errthunk(); }
+    else throw e;
+  }
+}
+
+DEP.bad = function () {
+  /* For use in a value-function: make the dep be bad. */
+  throw BAD;
+}
+
+function recompute_pending() {
+  /* Recompute any pending dependencies.
+   *
+   * The state is `recomputing' during this.
+   */
+
+  var d, f;
+
+  state = 'recomputing';
+  try {
+    while (PENDING.length) {
+      d = PENDING.shift();
+      f = d.__flags;
+      d.__flags = f & ~F_QUEUED;
+      if (!(f & F_VALUE))
+       d._recompute();
+      else if (!(f & F_DEPS)) {
+       d.new_value();
+       d.__flags = f | F_DEPS;
+      }
+    }
+  } finally {
+    while (PENDING.length) {
+      d = PENDING.shift();
+      d._value = BAD;
+    }
+  }
+}
+
+DEP.with_frozen = function (body, delay) {
+  /* Call BODY with dependencies frozen.
+   *
+   * If the BODY function changes any dep values (using D.set_value(...))
+   * then dependents won't be updated until the BODY completes.
+   *
+   * It's very
+   * bad to do this during a recomputation phase.  If DELAY is true, then the
+   * BODY is delayed until the recomputation completes; otherwise you get an
+   * exception.
+   */
+
+  var op, val;
+  var old_delayed, old_pending;
+
+  switch (STATE) {
+  case 'frozen':
+    body();
+    break;
+  case 'recomputing':
+    if (!delay) throw BusyRecomputing;
+    DELAYED.push(body);
+    break;
+  case 'ready':
+    old_delayed = DELAYED;
+    old_pending = PENDING;
+    try {
+      DELAYED = [];
+      PENDING = [];
+      GENERATION = new Generation('dep-generation');
+      val = body();
+      for (;;) {
+       recompute_pending();
+       if (!DELAYED.length) break;
+       op = DELAYED.shift();
+       op();
+      }
+    } finally {
+      DELAYED = old_delayed;
+      PENDING = old_pending;
+    }
+    break;
+  }
+}
+
+/*----- That's all, folks -------------------------------------------------*/
+} })();
diff --git a/rolling.css b/rolling.css
new file mode 100644 (file)
index 0000000..a1bbca5
--- /dev/null
@@ -0,0 +1,24 @@
+td.label { text-align: right; }
+.conceal { display: none; }
+h1 {
+       padding-bottom: 1ex;
+       border-bottom-style: solid;
+       border-width: medium;
+}
+h2 {
+       padding-top: 1ex;
+       margin-top: 2ex;
+}
+h1 + h2, h2:first-child {
+       border-top-style: hidden;
+       margin-top: 2ex;
+}
+.widgets {
+       float: left;
+       width: auto;
+       margin-right: 2em;
+       margin-bottom: 2em;
+}
+#toggle-help {
+       text-align: right;
+}
diff --git a/rolling.html b/rolling.html
new file mode 100644 (file)
index 0000000..fbee44e
--- /dev/null
@@ -0,0 +1,188 @@
+<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN"
+          "http://www.w3c.org/TR/html4/strict.dtd">
+<html>
+<head>
+  <title>Rolling wire-strip calculator</title>
+  <script type="text/javascript" src="dep.js"></script>
+  <script type="text/javascript" src="dep-ui.js"></script>
+  <link rel=stylesheet type="text/css" href="dep-ui.css">
+  <link rel=stylesheet type="text/css" href="rolling.css">
+<head>
+<body>
+
+<h1>Rolling wire-strip calculator</h1>
+
+<script type="text/javascript"><!--
+--></script>
+
+<table class=widgets>
+  <tr><td colspan=2><h3>Required size</h3>
+  <tr>
+    <td class=label><label for=width>Width: </label>
+    <td><input id=width>
+    <script type="text/javascript"><!--
+       var width = new DEP.Dep();
+       DEP_UI.input_field('width', width, DEP_UI.convert_to_numeric);
+    --></script>
+  <tr>
+    <td class=label><label for=thick>Thickness: </label>
+    <td><input id=thick>
+    <script type="text/javascript"><!--
+       var thick = new DEP.Dep();
+       DEP_UI.input_field('thick', thick, DEP_UI.convert_to_numeric);
+    --></script>
+  <tr>
+    <td class=label><label for=length>Length: </label>
+    <td><input id=length>
+    <script type="text/javascript"><!--
+       var length = new DEP.Dep();
+       DEP_UI.input_field('length', length, DEP_UI.convert_to_numeric);
+    --></script>
+
+  <tr><td colspan=2><h3>You should start with</h3>
+  <tr>
+    <td class=label><label for=sq-size>Square side: </label>
+    <td><input id=sq-size readonly>
+    <script type="text/javascript"><!--
+       var sq_size = new DEP.Dep(function () {
+         return Math.pow(Math.pow(width.value(), 2) * thick.value(), 1/3);
+       });
+       DEP_UI.output_field('sq-size', sq_size, DEP_UI.convert_from_numeric);
+    --></script>
+  <tr>
+    <td class=label><label for=rnd-diam>Round diameter: </label>
+    <td><input id=rnd-diam readonly>
+    <script type="text/javascript"><!--
+       var rnd_diam = new DEP.Dep(function () {
+         return 2*sq_size.value()/Math.sqrt(Math.PI);
+       });
+       DEP_UI.output_field('rnd-diam', rnd_diam,
+                           DEP_UI.convert_from_numeric);
+    --></script>
+  <tr>
+    <td class=label><label for=start-length>Length: </label>
+    <td><input id=start-length readonly>
+    <script type="text/javascript"><!--
+       var volume = new DEP.Dep(function () {
+         return width.value() * thick.value() * length.value();
+       });
+       volume.name = 'volume';
+       var start_length = new DEP.Dep(function () {
+         return volume.value()/(Math.pow(sq_size.value(), 2));
+       });
+       DEP_UI.output_field('start-length', start_length,
+                           DEP_UI.convert_from_numeric);
+    --></script>
+
+  <tr><td colspan=2><h3>Initial stock</h3>
+  <tr><td colspan=2>
+    <input type=radio name=stock-type value=round id=stock-round checked>
+    <label for=stock-round>Round section</label>
+    <script type="text/javascript"><!--
+       var stock_type = new DEP.Dep();
+       DEP_UI.input_radio('stock-round', stock_type);
+    --></script>
+  <tr><td colspan=2>
+    <input type=radio name=stock-type value=square id=stock-square>
+    <label for=stock-square>Square section</label>
+    <script type="text/javascript"><!--
+       DEP_UI.input_radio('stock-square', stock_type);
+    --></script>
+  <tr>
+    <td class=label><label for=stock-size>Stock size: </label>
+    <td><input id=stock-size>
+    <script type="text/javascript"><!--
+       var stock_size = new DEP.Dep();
+       DEP_UI.input_field('stock-size', stock_size,
+                          DEP_UI.convert_to_numeric);
+    --></script>
+  <tr>
+    <td class=label><label for=stock-length>Stock length: </label>
+    <td><input id=stock-length readonly>
+    <script type="text/javascript"><!--
+       var stock_length = new DEP.Dep(function () {
+         var cross;
+         switch (stock_type.value()) {
+           case 'round':
+             cross = 1/4 * Math.PI * Math.pow(stock_size.value(), 2);
+             break;
+           case 'square':
+             cross = Math.pow(stock_size.value(), 2);
+             break;
+           default:
+             dep_bad();
+         }
+         return volume.value()/cross;
+       });
+       DEP_UI.output_field('stock-length', stock_length,
+                           DEP_UI.convert_from_numeric);
+    --></script>
+</table>
+
+<script type="text/javascript"><!--
+       function toggle_help() {
+         var e = DEP_UI.elt('help');
+         var b = DEP_UI.elt('toggle-help');
+         if (e.className.match(/\bconceal\b/)) {
+           b.textContent = 'Hide help';
+           DEP_UI.rm_elt_class(e, 'conceal');
+         } else {
+           b.textContent = 'Show help';
+           DEP_UI.add_elt_class(e, 'conceal');
+         }
+       }
+       document.write(
+         '<button id=toggle-help onclick="toggle_help()">' + 
+         'Show help</button>');
+--></script>
+<div id=help>
+<script type="text/javascript"><!--
+       DEP_UI.add_elt_class(DEP_UI.elt('help'), 'conceal');
+--></script>
+<h2>What this program does</h2>
+
+<div id=js-note>
+<script type="text/javascript"><!--
+       DEP_UI.add_elt_class(DEP_UI.elt('js-note'), 'conceal');
+--></script>
+<p>This page is only really interesting because it contains a Javascript
+program.  You seem to have Javascript turned off, so it won't work very
+well.
+</div>
+
+<h3>Background</h3>
+
+<p>When you pass round or square wire through flat rolls it gets flatter
+and longer, but it also gets wider.  You can exploit this to make thin
+wire strip of almost any dimensions, but it&rsquo;s rather difficult to
+predict the result.  That&rsquo;s what this calculator does.
+
+<p>You specify the width and thickness of the strip you want, and the
+program calculates what size round or square wire you should start with.
+Additionally, if you specify the length of strip you need, it will
+calculate the length of the input wire.
+
+<p>The chances are that you don&rsquo;t actually have the required
+thickness of round or square wire to start with, but you do have some
+that&rsquo;s thicker.  Just enter the size of the round or square
+initial stock that you do have and the program will calculate how much
+of it you need to create the required starting size that you can then
+roll down to the required thickness of strip.
+
+<p>For best results, roll the strip in as few passes as you can handle.
+
+<h3>Use</h3>
+
+<p>Boxes with light red or white backgrounds are entry boxes for you to
+type in your requirements; boxes with dark red or grey backgrounds are
+where the calculator puts its answers.  White and grey mean that the box
+is showing useful information: an input box contains a valid number, for
+example, and an output box has calculated a correct answer.  Red, on the
+other hand, means that there&rsquo;s something wrong: either the
+calculator can&rsquo;t understand what you&rsquo;ve typed in an input
+box, or it&rsquo;s hit trouble &ndash; usually this means that some
+necessary input is missing.
+
+</div>
+</body>
+</html>