
/****************************************************************************

    Virtual nixie tube display, clock & calculator DHTML components

    v 1.05, 20080214a

    (c) 2007-08 Cestmir Hybl, cestmir.hybl@nustep.net
    http://cestmir.freeside.sk/projects/dhtml-nixie-display

    license: free for non-commercial use, copyright must be preserved

 ****************************************************************************/



/*   NixieDisplay   */

// public class NixieDisplay
function NixieDisplay()
{
  // public
  this.id = 'nixie';
  this.elContainer = null;
  this.charCount = 10;
  this.autoDecimalPoint = true;  // automatically extracts decimal point index in setText() call
  this.align = 'left';           // alignment of text via setText() call
  this.afterUpdate = null;   // after display update callback

  this.charWidth = 62;
  this.charHeight = 150;
  this.charGapWidth = 0;
  this.extraGapsWidths = [];
  this.createCharElements = true;

  this.text = '';
  this.decimalPoint = -1;

  this.urlCharsetImage = 'nixie/zm1082_l1_09bdm_62x150_8b.png';
  this.charMap = {
      0: 0,   1: 1,   2: 2,   3: 3,   4: 4,   5: 5,   6: 6,   7: 7,   8: 8,   9: 9,
    '0': 0, '1': 1, '2': 2, '3': 3, '4': 4, '5': 5, '6': 6, '7': 7, '8': 8, '9': 9, ' ': 10, '-': 11,
    'default': 10
  };
    // maps displayable chars onto glyph matrix indexes

  // protected
  function _drawChar(index)
  {
    var el = document.getElementById(this.id + '_d' + index);
    var charIndex = this.charMap[this.text.charAt(index)];
    if (!charIndex && charIndex !== 0)
      charIndex = this.charMap['default'];
    var x = - (charIndex * this.charWidth);
    var y = (index === this.decimalPoint ? - this.charHeight : 0);
    el.style.backgroundPosition = x + 'px ' + y + 'px';
  }
  this._drawChar = _drawChar;

  // Shows given string on display
  // public
  function setText(text, updateDecimalPoint)
  {
    // force string type
    this.text = text + '';

    // extract decimal point
    updateDecimalPoint = (typeof(updateDecimalPoint) != 'undefined' ? updateDecimalPoint : this.autoDecimalPoint);
    if (updateDecimalPoint) {
      var i = this.text.indexOf('.');
      if (i >= 0) {
        this.decimalPoint = i - 1;
        // alert(this.decimalPoint);
        this.text = this.text.substr(0, i) + this.text.substr(i + 1);
      } else
        this.decimalPoint = -1;
    }

    // pad up to display width (from left/right acording to this.align)
    if (this.text.length < this.charCount) {
      var pad = '';
      var padWidth = this.charCount - this.text.length;
      for (var i = 0; i < padWidth; i++)
        pad += ' ';
      if (this.align == 'left')
        this.text = this.text + pad;
      else {
        if (this.decimalPoint >= 0)
          this.decimalPoint += padWidth;
        this.text = pad + this.text;
      }
    }

    if (this.text.length > this.charCount)
      this.text = this.text.substr(0, this.charCount);

    // draw chars
    for (var i = 0; i < this.text.length; i++) {
      this._drawChar(i);
    }

    if (this.afterUpdate)
      this.afterUpdate(this);
  }
  this.setText = setText;

  // Sets char at given display position
  // public
  function setChar(index, chr)
  {
    // alert(chr);
    this.text = this.text.substring(0, index) + chr + this.text.substring(index + 1);
    this.setText(this.text, false);
  }
  this.setChar = setChar;

  function setDecimalPoint(index)
  {
    var oldDecimalPoint = this.decimalPoint;
    this.decimalPoint = ((!index && index !== 0) ? -1 : index);
    if (oldDecimalPoint != this.decimalPoint) {
      if (oldDecimalPoint >= 0)
        this._drawChar(oldDecimalPoint);
      if (this.decimalPoint >= 0)
        this._drawChar(this.decimalPoint);
    }
  }
  this.setDecimalPoint = setDecimalPoint;

  // Clears display - fills all positions with given char (space by default).
  // public
  function clear(chr)
  {
    chr = (typeof(chr) == 'undefined' ? ' ' : chr);
    this.text = '';
    for (var i = 0; i < this.charCount; i++)
      this.text += chr;
    this.decimalPoint = -1;
    this.setText(this.text);
  }
  this.clear = clear;

  // Shifts display contents left or right
  // public
  function shift(direction, step)
  {
    step = (!step && step !== 0 ? 1 : step);
    direction = (!direction ? 'left' : direction);

    if (this.decimalPoint >= 0) {
      this.decimalPoint += (direction == 'left' ? - step : + step);
      if (this.decimalPoint >= this.charCount)
        this.decimalPoint = -1;
    }

    if (direction == 'left')
      this.text = this.text.substr(step) + ' '; // @todo padding for step != +/-1
    else if (direction == 'right')
      this.text = ' ' + this.text.substr(0, this.text.length - 1); // @todo padding for step != +/-1
    this.setText(this.text, false);
  }
  this.shift = shift;

  // public
  function init()
  {
    if (!this.elContainer) {
      this.elContainer = document.getElementById(this.id);
      if (!this.elContainer)
        throw "Container element '" + this.id + "' not found";
    }
    this.elContainer.style.position = 'relative';

    if (this.createCharElements) {
      var totalWidth = 0;
      for (var i = 0; i < this.charCount; i++) {
        var charWidthIncludingGap = (this.charWidth + this.charGapWidth);

        var elId = this.id + '_d' + i;
        var el0 = document.getElementById(elId);
        var el = (el0 ? el0 : document.createElement('div'));
        el.id = this.id + '_d' + i;
        el.className = 'digit d' + i;
        el.style.position = 'absolute';
        el.style.left = totalWidth + 'px';
        el.style.width = this.charWidth + 'px';
        el.style.height = this.charHeight + 'px';
        el.style.background = 'url(' + this.urlCharsetImage + ')';
        if (!el.parentNode)
          this.elContainer.appendChild(el);

        totalWidth += charWidthIncludingGap + (this.extraGapsWidths[i] ? this.extraGapsWidths[i] : 0);
      }
      this.elContainer.style.width = totalWidth + 'px';
      this.elContainer.style.height = this.charHeight + 'px';
    }

    if (this.text)
      this.setText(this.text)
    else
      this.clear();
  }
  this.init = init;
}



/*   NixieClock   */

// public class NixieClock : NixieDisplay
function NixieClock()
{
  // public

  // private
  this.lastSeconds = -1;

  // Show current time on "display"
  // public
  function showCurrentTime(refreshAfterChangeOnly)
  {
    var d = new Date();

    var s = d.getSeconds();

    if (refreshAfterChangeOnly && s == this.lastSeconds)
      return;
    else
      this.lastSeconds = s;

    var h = d.getHours();
    var m = d.getMinutes();

    var digits = '';

    digits += (h / 10) | 0;
    digits += h % 10;
    digits += (m / 10) | 0;
    digits += m % 10;
    digits += (s / 10) | 0;
    digits += s % 10;

    this.setText(digits);
  }
  this.showCurrentTime = showCurrentTime;

  // Run clock (via scheduling a periodic callback to showCurrentTime())
  // public
  function run()
  {
    if (!this.elContainer)
      this.init();
    var __nixieClock = this;
    window.setInterval(function() { __nixieClock.showCurrentTime(true); }, 100);
  }
  this.run = run;

  this.ancestor = NixieDisplay;
  this.ancestor();

  this.charCount = 6;
  this.extraGapsWidths[1] = 20;
  this.extraGapsWidths[3] = 20;
}



/*  NixieCalculator  */

// @todo rounding of rightmost digit

// public class NixieCalculator : NixieDisplay
function NixieCalculator()
{
  // public
  this.id = 'nixieCalc';
  this.digitCount = 13;
  this.display = new NixieDisplay();

  // private
  this.operandStack = [];
  this.newValueAtNextChar = false;
  this.fullPrecisionValue = 0;

  // private
  function push(value)
  {
    this.operandStack[this.operandStack.length] = value; // JS50 compatible .push()
  }
  this.push = push;

  // private
  function pop()
  {
    if (!this.operandStack.length)
      return null;
    var v = this.operandStack[this.operandStack.length - 1];
    this.operandStack = this.operandStack.slice(0, this.operandStack.length - 1); // JS50 compatible .pop()
    return v;
  }
  this.pop = pop;

  // public
  function getValue()
  {
    if (this.fullPrecisionValue !== null)
      return this.fullPrecisionValue;

    var v = this.display.text;

    // insert decimal point
    if (this.display.decimalPoint >= 0 && this.display.decimalPoint < this.digitCount - 1)
      v = v.substr(0, this.display.decimalPoint + 1) + '.' + v.substr(this.display.decimalPoint + 1);

    // remove padding spaces
    var i = 0;
    while (i < v.length && v.charAt(i) == ' ')
      i++;
    v = v.substr(i);

    // convert to number
    v = parseFloat(v);

    return v;
  }
  this.getValue = getValue;

  // public
  function setValue(v)
  {
    if (typeof(v) != 'number')
      v = parseFloat(v);
    if (isNaN(v) || v > this.maxNumber || v < -this.maxNumber)
      this.error();
    else {
      this.fullPrecisionValue = v;
      if (v.toFixed) {
        // force fixed-point notation (JS5.5+)
        var s = (v >= 0 ? ' ' : '') + v.toFixed(1);
        s = s.substring(0, s.length - 2);
        // (s now contains string with integer part of value, prefixed by either ' ' or '-')
        v = v.toFixed(this.digitCount - s.length); // to fixed point + round rightmost digit
      } else {
        v = v.toString();
        if (v.toLowerCase().indexOf('e') >= 0) {
          // we won't handle exp notation in JS<5.5
          this.error();
          return;
        }
      }
      if (v !== '0') {
        if (v.charAt(0) != '-')
          v = ' ' + v;
        var c = this.digitCount + (v.indexOf('.') >= 0 ? 1 : 0);
        if (v.length > c)
          v = v.substr(0, c);
        v = v.replace(/^(.{1,}?)\.?0+$/g, '$1'); // strip zero's from right
      }
      this.display.setText(v);
    }
  }
  this.setValue = setValue;

  // private
  function eval(v1, o, v2)
  {
    try {
      switch (o) {
        case '+':
          return v1 + v2;
        case '-':
          return v1 - v2;
        case '*':
          return v1 * v2;
        case '/':
          return v1 / v2;
        case '^':
          return Math.pow(v1, v2);
        case 'sqrt':
          return Math.sqrt(v1);
        case 'sqr':
          return v1 * v1;
        default:
          throw "Unsupported operand: '" + o + "'";
      }
    } catch(e) {
      this.error();
    }
  }
  this.eval = eval;

  // public
  function error()
  {
    var s= '';
    for (var i = 0; i < this.digitCount; i++)
      s += '-';
    this.operandStack = [];
    this.newValueAtNextChar = true;
    this.fullPrecisionValue = null;
    this.display.setText(s);
  }
  this.error = error;

  // public
  function clear()
  {
    this.display.clear();
    this.setValue(0);
    this.operandStack = [];
    this.fullPrecisionValue = null;
  }
  this.clear = clear;

  // public
  function keyDown(event0)
  {
    var e = (event0 ? event0 : event);
    var k = e.keyCode;

    var cancelEvent = true;

    if (k == 8) {
      // backspace
      if (this.display.text.charAt(this.digitCount - 2) == ' ')
        this.display.setChar(this.digitCount - 1, '0');
      else
        this.display.shift('right');
      this.fullPrecisionValue = null;
    } else if (k == 27) {
      // escape
      this.clear();
    } else
      cancelEvent = false;

    return !cancelEvent;
  }
  this.keyDown = keyDown;

  // public
  function keyPress(event0)
  {
    var e = (event0 ? event0 : event);
    var k = (e.keyCode ? e.keyCode : e.which); // IE: .keyCode, FF: .which
    var chr = String.fromCharCode(k);

    var cancelEvent = true;

    var newValueAtThisChar =  this.newValueAtNextChar;
    this.newValueAtNextChar = true;

    if (chr >= '0' && chr <= '9') {
      this.fullPrecisionValue = null;
      if (newValueAtThisChar) {
        this.display.clear();
      }
      if (this.display.text.charAt(1) == ' ' || this.display.text.charAt(1) == '-') {
        if (this.display.text.charAt(this.digitCount - 1) == '0' && this.display.text.charAt(this.digitCount - 2) == ' ' && this.display.decimalPoint < 0)
          ;
        else
          this.display.shift('left');
        this.display.setChar(this.digitCount - 1, chr);
      }
      this.newValueAtNextChar = false;
    }
    else if (chr == '.' || chr == ',') {
      this.fullPrecisionValue = null;
      if (newValueAtThisChar)
        this.display.setText(0);
      if (this.display.decimalPoint < 0)
        this.display.setDecimalPoint(this.digitCount - 1);
      this.newValueAtNextChar = false;
    }
    else if (chr == '+' || chr == '-' || chr == '*' || chr == '/' || chr == '^') {
      if (this.operandStack.length > 2)
        // cancel repeated evaluation
        this.operandStack = [];
      if (this.operandStack.length == 2) {
        // previous expression without explicit '=', evaluate it
        this.setValue(this.eval(this.operandStack[this.operandStack.length - 2], this.operandStack[this.operandStack.length - 1], this.getValue()));
        this.operandStack = [];
      }
      // push left operand
      this.push(this.getValue());
      // push operator
      this.push(chr);
    }
    else if (chr == 'm' || chr == 'M') {
      this.setValue(- this.getValue());
      this.newValueAtNextChar = false;
    }
    else if (chr == 'p' || chr == 'P') {
      this.setValue(Math.PI);
    }
    else if (chr == 'q') {
      this.setValue(this.eval(this.getValue(), 'sqrt', null));
    }
    else if (chr == 'Q') {
      this.setValue(this.eval(this.getValue(), 'sqr', null));
    }
    else if (k == 13 || chr == '=') {
      if (this.operandStack.length >= 2) {
        if (this.operandStack.length <= 2)
          // push right operand
          this.push(this.getValue())
        else
          // repeated evaluation (e.g. [1] [+] [1] [=] [=] ...), replace left operand with current display value
          this.operandStack[this.operandStack.length - 3] = this.getValue();
        // alert(this.operandStack);
        var result = this.eval(this.operandStack[this.operandStack.length - 3], this.operandStack[this.operandStack.length - 2], this.operandStack[this.operandStack.length - 1]);
        this.setValue(result);
        // this.operandStack = [];
      }
    }
    // else if (k == 27) {  // Fix: chromium: Esc not handled in keyPress(), http://code.google.com/p/chromium/issues/detail?id=12744
    //   // escape
    //   this.clear();
    // }
    else {
      cancelEvent = false;
      this.newValueAtNextChar = false;
    }

    return !cancelEvent;
  }
  this.keyPress = keyPress;

  // public
  function init()
  {
    this.display.id = this.id;
    this.display.charCount = this.digitCount;
    this.display.align = 'right';
    this.display.init();
    this.display.setText('0');
    this.maxNumber = Math.pow(10, this.digitCount - 1) - 1;
  }
  this.init = init;

  // assign default glyph matrix
  this.display.urlCharsetImage = 'nixie/zm1080_l2_09bdm_45x75_8b.png';
  this.display.charWidth = 45;
  this.display.charHeight = 75;
  this.display.charGapWidth = 5;
}
