var jsMedian = {

  vesion: "1.0",

  /*********************************************/
  /*
   *  Manage JavaScript objects:
   *  
   *      Add:        add/replace items in an object
   *      Insert:     recursively insert into an objec
   *      Prototype:  add items to an object prototype
   */
  Add: function (dst,src) {for (var id in src) {dst[id] = src[id];}},
  Prototype: function (obj,def) {this.Add(obj.prototype,def);},
  Insert: function (dst,src) {
    for (var id in src) {
      if (typeof dst[id] !== 'unknown'
            &&  typeof src[id] === 'object'
            && (typeof dst[id] === 'object'
            ||  typeof dst[id] === 'function')) {
        this.Insert(dst[id],src[id]);
      } else {
        dst[id] = src[id];
      }
    }
  },

  /*********************************************/
  /*
   *  Manage events uniformly
   */
  getEvent: function (event) {
    if (!event) {event = window.event;}
    event.complete = jsMedian.endEvent;
    // MSIE and Opera don't timestamp their events, so save our own
    event.time = (event.timeStamp || new Date().getTime());
    return event;
  },
    
  endEvent: function () {
    var event = this;
    if (event.preventDefault) {event.preventDefault();}
    if (event.stopPropagation) {event.stopPropagation();}
    event.cancelBubble = true;
    event.returnValue = false;
    return false;
  },
  
  /*********************************************/
  /*
   *  Create an HTML element with initialization
   */
  Element: function (type,def) {
    var obj = document.createElement(type);
    for (var i = 1; i < arguments.length; i++) {
      if (arguments[i]) {jsMedian.Insert(obj,arguments[i]);}
    }
    return obj;
  }

};

/***********************************************************************/
/*
 *  Object-Oriented programming model
 */

jsMedian.Object = function () {};
jsMedian.Add(jsMedian.Object,{
  Subclass: function (def,classdef) {
    var obj = function () {
      if (arguments.length != 1 || arguments[0] !== jsMedian.Object.NOINIT)
        {this.Init.apply(this,arguments);}
    };
    obj.SUPER = this;
    obj.Subclass = this.Subclass; obj.Augment = this.Augment;
    obj.protoFunction = this.protoFunction;
    obj.prototype = new this(jsMedian.Object.NOINIT);
    obj.prototype.constructor = obj;  // get real constructor
    obj.Augment(def,classdef);
    return obj;
  },
  
  Augment: function (def,classdef) {
    if (def !== null) {
      // MSIE doesn't show the toString method even if it is not native
      // so handle it separately
      for (var id in def) {if (id !== 'toString') {this.protoFunction(id,def[id]);}}
      if (!def.toString.toString().match(/\[native code\]|^\(Internal Function\)/i))
        {this.protoFunction('toString',def.toString);}
    }
    if (classdef !== null) {jsMedian.Add(this,classdef);}
  },
  
  protoFunction: function (id,def) {
    this.prototype[id] = def;
    if (def instanceof Function) {def.SUPER = this.SUPER.prototype;}
  },
  
  prototype: {
    Init: function () {},
    SUPER: function (fn) {return fn.callee.SUPER;}
  },
  
  NOINIT: {noinit:1}
});

/***********************************************************************/
/***********************************************************************/
/*
 *  The main simulation object
 */

jsMedian.Simulation = jsMedian.Object.Subclass({
  defaults: {
    width: 475, height: 350,  // size of the initial field
    background: "white",      // background color
    pointSize: 9,             // size of points and center
    fontSize: 100,            // size of point weight labels
    delay: 25,                // delay between steps
    weight: 1.2,              // weight of the center point
    friction: 0.15,           // friction of surface
    K: 10,                    // scaling factor for weight
    MINW: 100, MINH: 60,      // minimum field size
    MAXW: null, MAXH: null    // maximum field size (if non-null)
  },
  flags: {
    points: "",               // initial point list
    model: "Mediancentre",    // initial model to use
    position: null,           // initial position (if non-null)
    velocity: "(0.00, 0.00)", // initial velocity
    autoStart: 1,
    autoRestart: 1,
    showPosition: 1,
    showTrail: 0,
    showConnections: 1,
    showShortest: 0,
    
    showDrawer: 0,            // initial position of drawer
    showDrawerPull: 1,        // allow drawer to open and close?
    fixInitialPoints: 0,      // allow editing of initial configuration?
    allowWeightChange: 1,     // allow editing of point weights?
    allowZeroWeight: 0,       // allow points with weight 0?
    allowNewPoints: 1,        // allow new points to be added?
    allowTransformations: 1,  // allow translation, scaling, rotation?
    allowResize: 1,           // allow canvas resize?
    allowModelChange: 1       // allow switch between mean and median?
  },

  drawerOpen: 0,            // 0 = closed, 1 = open
  running: 0,               // is simulation running?
  started: 0,               // has the simulation run before (for auto-restart)?
  stopping: 0,              // are we trying to stop?
  drawTime: 0,              // used to discount display updaate in step time
  
  /*********************************************/
  /*
   *  Create a new simulation environment
   */
  Init: function (container,options) {
    this.id = jsMedian.Simulation.ID();

    if (!options && typeof container === 'object')
      {options = container; container = undefined;}
    this.options = (options || {});
    this.setDefaults();

    //  Insert a div if we don't have a place specified
    if (typeof container === 'undefined') {
      document.write('<div id="'+this.id+'_div"></div>');
      container = this.id+'_div';
    }
    //  Find the specified container element
    if (typeof container === 'string') 
      {container = document.getElementById(container);}

    //  Fill the container with the simulation HTML
    this.createElements(container,
      ["<table>",{cellSpacing: 0, cellPadding: 0},["<tbody>",["<tr>",["<td>",[
       ["<div>",{className: "jsMedianContainer",
                 sData: this, id: this.id,
                 style: {position: "relative", overflow: "hidden",
                         background: this.background,
                         width: this.width+"px", height: this.height+"px"}},
        [".trail",".canvas",".dots",".mbox",[".help",["?"]],".resize"]
       ], // div (container)
       [".drawer",{style: {width: this.width+"px"}},
        [".table",
         ["<tbody>",
          ["<tr>",{vAlign:"top"},
           [
            ["<td>",{className:"jsMedianPanel jsMedianPoints"},
             [
              "Points:","<br>",".points",
              ["<div>",{className:"jsMedianSpacing"}],
              ["<center>",[".set"," ",".rescale"]]
             ]
            ], // td
            ["<td>",{className:"jsMedianPanel jsMedianPosition"},
             [
              [".model",[
                ["<option>",{value:"Mediancentre"},["Mediancentre"]],
                ["<option>",{value:"Mean"},["Mean"]]
              ]],"<br>",
              "Position: ",".position","<br>",
              "Velocity: ",".velocity",
              "<hr>",
              "Weight: ",[".weight",{value: this.weight}],"<br>",
              "Friction: ",[".friction",{value: this.friction}]
             ]
            ], // td
            ["<td>",{className:"jsMedianPanel jsMedianShow"},
             [
              [".autostart",{checked:this.getOption("autoStart")}],"Auto-Start","<br>",
              [".autorestart",{checked:this.getOption("autoRestart")}],"Auto-Restart",
              ["<div>",{className:"jsMedianSpacing"}],
              "Show:","<br>",
              [".showpos",{checked:this.getOption("showPosition")}],"Position","<br>",
              [".showtrail",{checked:this.getOption("showTrail")}],"Point Trail","<br>",
              [".showlines",{checked:this.getOption("showConnections")}],"Connections","<br>",
              [".showshort",{checked:this.getOption("showShortest")}],"Shortest"
             ]
            ], // td
            ["<td>",{className:"jsMedianPanel jsMedianButtons"},
             [
              ".start","<br>",
              ".step","<br>",
              ".stop","<br>",
              ".clear",
              "<hr>",
              ".save"
             ]
            ] // td
           ]
          ]  // tr
         ]  // tbody
        ]  // table
       ], // drawer
       [".pulldown",{style:{width:this.width+"px"}},["\xA0"]]
      ]]]]]
    );

    //  Initialize the graphcs and points
    this.graph = new jsGraphics(this.control("canvas"));
    this.trail = new jsGraphics(this.control("trail"));
    this.dots  = new jsMedian.DotList(this.id);
    this.point = new jsMedian.Point(this.id,this.control("mbox"),
                                    this.width/2,this.height/2);
    
    //  Initialize to user values
    this.control("showpos").onclick();
    this.control("showlines").onclick();
    this.control("showshort").onclick();
    this.model = this.control("model").value = this.getOption("model");
    this.setTypeIns();
    if (!this.getOption("showDrawerPull"))
      {this.control("pulldown").style.display = "none";}
    if (!this.getOption("allowResize"))
      {this.control("resize").style.display = "none";}
    if (this.getOption("fixInitialPoints")) {
      this.control("clear").value = "Reset";
      this.disable("points","set");
    }
    if (!this.getOption("allowModelChange")) {this.disable("model");}
    if (!this.getOption("allowTransformations")) {this.disable("rescale");}
    
    // 
    // Hack to get around bug in MSIE, which can't pass arguements
    // to the code called by setTimeout.  We pass it as a property
    // of the function called instead.
    // 
    this._step = new Function("arguments.callee.sim.step()");
    this._pull = new Function("arguments.callee.sim.pull()");
    this._step.sim = this._pull.sim = this;

    if (this.getOption("showDrawer")) {
      this.control("table").style.position = "";
      this.control("drawer").style.height = "";
      this.drawerOpen = 1;
    }
  },
  
  /*********************************************/
  /*
   *  Get values from user options or defaults
   */
  setDefaults: function () {
    for (var id in this.defaults) {
      this[id] = (typeof this.options[id] !== 'undefined' ?
                    this.options[id]: this.defaults[id]);
    }
  },
  
  getOption: function (id) {
    if (typeof this.options[id] !== 'undefined') {return this.options[id];}
    if (typeof this.flags[id] !== 'undefined') {return this.flags[id];}
    return this.defaults[id];
  },

  /*********************************************/
  /*
   *  Enable or disable collections of controls
   */
  enable: function () {
    for (var i = 0; i < arguments.length; i++) 
      {this.control(arguments[i]).disabled = 0;}
  },
  disable: function () {
    for (var i = 0; i < arguments.length; i++) 
      {this.control(arguments[i]).disabled = 1;}
  },
  
  /*********************************************/
  /*
   *  Get a control element by name
   */
  control: function (name) {return document.getElementById(this.id+"_"+name);},
  
  /*********************************************/
  /*
   *  Create an HTML tree
   */
  createElements: function (container,def) {
    var name, options, contents;
    if (typeof def === 'string') {name = def;} else {
      if (def.length === 0) {return;}
      if (typeof def[0] === 'string' && def[0].substr(0,1).match(/[<.]/)) {
        if (def.length === 2 && typeof def[1] == 'object') {
          name = def[0];
          if (typeof def[1].length === 'undefined') 
            {options = def[1];} else {contents = def[1];}
        }
        if (def.length === 3 && typeof def[1] === 'object'
                             && typeof def[1].length === 'undefined'
                             && typeof def[2] === 'object'
                             && typeof def[2].length !== 'undefined') {
          name = def[0]; options = def[1]; contents = def[2];
        }
      }
    }
    if (typeof name === 'undefined') {
      for (var i = 0; i < def.length; i++) {this.createElements(container,def[i]);}
    } else {
      var obj, c = name.substr(0,1);
      if (c === '<') {
        obj = jsMedian.Element(name.substr(1,name.length-2),options);
        obj.simulation = this.elementSimulation;
      } else if (c === '.') {
        name = name.substr(1);
        def = this.controls[name];
        obj = jsMedian.Element(def[0],def[1],{sID: this.id, id: this.id+'_'+name},options);
        obj.simulation = this.elementSimulation;
      } else {
        obj = document.createTextNode(name);
      }
      container.appendChild(obj);
      if (options && options.checked) {obj.checked = 1;} // MSIE unchecks it when appended!
      if (typeof contents != 'undefined') {this.createElements(obj,contents);}
    }
  },
  
  elementSimulation: function () {return jsMedian.Simulation.dataFor(this.sID);},
  
  /*********************************************/
  /*
   *  The definitions of the various controls and their functions
   */
  
  controls: {
    
    /*********************************************/
  
    canvas: ["div",{
      style: {position: "relative", overflow: "hidden", cursor: "default",
              width: "100%", height: "100%"}
    }],

    dots: ["div",{
      onmousedown: function (event) {
        event = jsMedian.getEvent(event);
        var x = event.offsetX; var y = event.offsetY;
        if (typeof x === "undefined") {x = event.layerX; y = event.layerY;}
        var simulation = this.simulation();
        if (event.altKey && event.shiftKey) {simulation.rotateField(x,y,event);}
        else if (event.altKey) {simulation.scaleField(x,y,event);}
        else if (event.shiftKey) {simulation.dragField(x,y,event);}
        else {simulation.addPoint(x,y,event);}
        event.complete();
      },
  
      ondblclick: function (event) {
        event = jsMedian.getEvent(event);
        var simulation = this.simulation();
        if (event.shiftKey) {
          if (simulation.getOption("allowTransformations")) {simulation.normalize();}
        } else if (event.altKey) {simulation.clear();}
        event.complete();
      },
  
      onselectstart: function () {return false;}, // For MSIE
  
      style: {position: "absolute", overflow: "hidden", cursor: "default",
              width: "100%", height: "100%", left: "0px", top: "0px"}
    }],
    
    /*********************************************/
  
    trail: ["div",{
      style: {position: "absolute", top: "0px", left: "0px",
              width: "100%", height: "100%"}
    }],
 
    mbox: ["div",{
      style: {position: "absolute", left: "0px", top: "0px",
              width: "0px", height: "0px"}
    }],
    
    /*********************************************/
  
    drawer: ["div",{
      style: {height: "0px", border: "none",
              position:"relative", overflow: "hidden"}
    }],
    
    table: ["table",{
      className: "jsMedianTable", border: 0, cellSpacing: 0, cellPadding: 0,
      style: {position: "relative", top: "0px"}
    }],
    
    pulldown: ["div",{
      className: "jsMedianDrawer",
      style: {height: "3px", border: "2px groove", lineHeight: "1px"},
      onmouseover: function () {this.style.background = "#DDDDDD";},
      onmouseout: function () {this.style.background = "";},
      onclick: function () {this.simulation().pullDrawer();},
      title: "click to reveal/hide control drawer"
    }],
    
    /*********************************************/
  
    points: ["textarea",{
      rows: 6, cols: 15,
      title: "Enter positions and weights for the points:\n" +
             "(x,y) or  w @ (x,y) separated by spaces or line breaks",
      onkeypress: function (event) {
        event = jsMedian.getEvent(event);
        if (event.shiftKey && (event.keyCode || 0) == 13) {
          this.simulation().dots.setPoints(this.value);
          event.complete();
        }
      },
      set: function () {this.simulation().dots.setPoints(this.value);}
    }],
    
    set: ["input",{
      type: "button", value: "Set",
      title: "Redefine points based on entries above",
      onclick: function () {
        var simulation = this.simulation();
        simulation.dots.setPoints(simulation.control("points").value);
      }
    }],
    
    rescale: ["input",{
      type: "button", value: "Rescale",
      title: "Rescale points to fill the available area",
      onclick: function () {this.simulation().normalize();}
    }],
    
    /*********************************************/
  
    model: ["select",{
      title: "Choose the model to use for the simulation",
      onchange: function () {
        var simulation = this.simulation();
        simulation.model = this.value;
        simulation.restart();
      }
    }],
    
    position: ["input",{
      type: "text", className: "jsMedianWideInput",
      title: "The position of the moving point",
      onchange: function () {this.simulation().setPosition(this.value);}
    }],
    
    velocity: ["input",{
      type: "text", className: "jsMedianWideInput",
      title: "The velocity of the moving point",
      onchange: function () {this.simulation().setVelocity(this.value);}
    }],
    
    weight: ["input",{
      type: "text", title: "The weight of the moving point",
      onchange: function () {this.simulation().setWeight(this.value);}
    }],
    
    friction: ["input",{
      type: "text", title: "The friction for the moving point",
      onchange: function () {this.simulation().setFriction(this.value);}
    }],
    
    /*********************************************/
  
    autostart: ["input",{
      type: "checkbox",
      title: "Start the simulation whenever the center point is moved"
    }],
    
    autorestart: ["input",{
      type: "checkbox", className: "jsMedianAutoRestart",
      title: "Restart a stopped simulation whenever the configuration is changed"
    }],
    
    showpos:   ["input",{
      type: "checkbox", className: "jsMedianShowCheckbox",
      title: "Show the position of the moving center point",
      onclick: function () {
        if (this.checked) {this.simulation().point.div().style.visibility = "visible";}
                     else {this.simulation().point.div().style.visibility = "hidden";}
      }
    }],
    
    showtrail: ["input",{
      type: "checkbox", className: "jsMedianShowCheckbox",
      title: "Show trail of past positions of the moving point"
    }],

    showlines: ["input",{
      type: "checkbox", className: "jsMedianShowCheckbox",
      title: "Show the lines connecting the center to the fixed points",
      onclick: function () {
        var simulation = this.simulation();
        if (this.checked) {
          simulation.dots.resumeDraw();
          simulation.dots.drawLines();
        } else {
          simulation.graph.clear();
          simulation.graph.paint();
          simulation.dots.suspendDraw();
        }
      }
    }],
    
    showshort: ["input",{
      type: "checkbox", className: "jsMedianShowCheckbox",
      title: "Highlight the connection to the closest fixed point",
      onclick: function () {
        var simulation = this.simulation();
        simulation.dots.showShortest = this.checked;
        simulation.dots.drawLines();
      }
    }],
    
    /*********************************************/
  
    start: ["input",{
      type: "button", value: "Start", disabled: 1,
      title: "Run the simulation",
      onclick: function () {this.simulation().start();}
    }],
    
    step: ["input",{
      type: "button", value: "Step", disabled: 1,
      title: "Take one step in the simulation",
      onclick: function () {
        var simulation = this.simulation();
        simulation.stop();
        simulation.step();
      }
    }],
    
    stop: ["input",{
      type: "button", value: "Stop", disabled: 1,
      title: "Stop the simulation from running",
      onclick: function () {this.simulation().stop();}
    }],
    
    clear: ["input",{
      type: "button", value: "Clear", disabled: 1,
      title: "Remove all fixed points and recenter the moving point",
      onclick: function () {this.simulation().clear();}
    }],
    
    save: ["input",{
      type: "button", value: "Save",
      title: "Produce JavaScript that can be pasted into an HTML page",
      onclick: function () {this.simulation().save();}
    }],
    
    /*********************************************/
    
    help: ["div",{
      onclick: function () {},
      title: "Open a window of information on how to use this simulation",
      className: "jsMedianHelp",
      style: {position: "absolute", display: "none"}
    }],
    
    /*********************************************/
  
    resize: ["div",{
      onmouseout:  function () {this.style.background = "";},
      onmouseover: function () {this.style.background = "#DDDDDD";},
      onmousedown: function (event) {
        event = jsMedian.getEvent(event);
        var x = event.offsetX; var y = event.offsetY;
        if (typeof x === "undefined") {x = event.layerX; y = event.layerY;}
        this.simulation().resizeField(x,y,event);
        event.complete();
      },
      title: "Resize the simulation window",
      className: "jsMedianResize",
      style: {position: "absolute", lineHeight: "1px"}
    }]
  },
  
  /*********************************************/
  /*
   *  Start or restart the simulation
   */
  start: function () {
    this.running = this.started = 1; this.stopping = 0;
    this.disable("start"); this.enable("step","stop");
    setTimeout(this._step,1);
  },
    
  restart: function () {
    if (this.started && !this.running) {this.start();}
  },
  
  /*********************************************/
  /*
   *  Perform a step in the simulation
   */
  step: function () {
    if (this.stopping) {this.stopping = 0; return;}
    var Fx = 0, Fy = 0; var m = 2*this.K, px, py;
    var P = this.point; var dot = this.dots.first;
    // the time step is reduced in the vicinity of a point
    var t = Math.sqrt(Math.min(5,Math.max(0.001,this.shortest)) / 5);
    if (this.model === 'Mean') {t = 1;}
    var tw = Math.pow(Math.max(1,this.dots.totalWeight),0.75);
    while (dot) {
      px = dot.x - P.x; py = dot.y - P.y;
      if (this.model === 'Mediancentre') {m = Math.sqrt(px*px + py*py);}
      if (m !== 0) {
        Fx = Fx + dot.weight * px / m;
        Fy = Fy + dot.weight * py / m;
      }
      dot = dot.next;
    }
    var V = this.point.velocity;
    V.x = (1-this.friction) * (V.x + t * this.K * Fx / this.weight / tw);
    V.y = (1-this.friction) * (V.y + t * this.K * Fy / this.weight / tw);
    P.trackTrail(P.x+t*V.x,P.y+t*V.y);
    if (this.running) {
      m = Math.sqrt(V.x*V.x + V.y*V.y);
      if (m < 0.0075) {this.stop();} else {
        var delay = Math.max(1,Math.floor(t*(this.delay - this.drawTime)));
        setTimeout(this._step,delay);
      }
    }
  },
  
  /*********************************************/
  /*
   *  Stop a running simulation
   */
  stop: function () {
    if (this.running) {
      this.stopping = 1;
      this.running = 0;
      if (this.dots.first) {this.enable("start");}
      this.disable("stop");
      this.point.clearTrail();
    }
  },
  
  /*********************************************/
  /*
   *  Clear the field and reset to initial conditions
   */
  clear: function () {
    this.started = 0;
    this.stop();
    this.dots.clear();
    this.point.velocity.x = this.point.velocity.y = 0;
    this.normalize();
    if (this.getOption("fixInitialPoints")) {this.setTypeIns();}
    if (!this.dots.first) {this.disable("start","step","stop");}
  },

  /*********************************************/
  /*
   *  Set the values for the type-in boxes
   */
  setTypeIns: function () {
    var typein = ["points","position","velocity"];
    for (var i in typein) {
      var value = this.getOption(typein[i]);
      if (value) {
        var control = this.control(typein[i]); control.value = value;
        if (control.onchange) {control.onchange();} else {control.set();}
      }
    }
  },
  
  /*********************************************/
  /*
   *  Set the position/velocity from the type-in areas
   */
  setPosition: function (string) {
    var result = string.match(/^ *\( *([\-+]?(?:\d+(?:\.\d*)?|\.\d+)) *, *([\-+]?(?:\d+(?:\.\d*)?|\.\d+)) *\) *$/);
    if (result) {this.point.moveto(parseFloat(result[1]),parseFloat(result[2]));}
  },
  
  setVelocity: function (string) {
    var result = string.match(/^ *\( *([\-+]?(?:\d+(?:\.\d*)?|\.\d+)) *, *([\-+]?(?:\d+(?:\.\d*)?|\.\d+)) *\) *$/);
    if (result) {
      this.point.velocity.x = parseFloat(result[1]);
      this.point.velocity.y = parseFloat(result[2]);
    }
    this.point.showVelocity();
  },
  
  /*********************************************/
  /*
   *  Set the weight/friction from the type0in areas
   */
  setWeight: function (string) {
    var w = parseFloat(string);
    if (w.toString() !== "NaN") {this.weight = w;}
    this.control("weight").value = this.weight;
  },
  
  setFriction: function (string) {
    var f = parseFloat(string);
    if (f.toString() !== "NaN") {this.friction = Math.max(0,Math.min(1,f));}
    this.control("friction").value = this.friction;
  },
  
  /*********************************************/
  /*
   *  Write a cut-and-paste version of the data
   *  to a new window
   */
  save: function () {
    var w = window.open("","_blank","width=500,height=400," +
        "directories=no,location=no,menubar=no,status=no");
    var doc = w.document;

    var option = {}; jsMedian.Add(option,this.options);
    var div = this.div();
    var W = parseInt(div.style.width,10), H = parseInt(div.style.height,10);
    if (W != this.getOption("width")) {option.width = W;}
    if (H != this.getOption("height")) {option.height = H;}
    option.points = this.control("points").value.replace(/\n/g," ");
    if (option.points === "") {delete option.points;}
    var typein = ["model","position","velocity","weight","friction"];
    var checkbox = {
      autoStart: "autostart", autoRestart: "autorestart", showPosition: "showpos",
      showTrail: "showtrail", showConnections: "showlines", showShortest: "showshort"
    };
    var value, id, i;
    for (i in typein) {
      value = this.control(typein[i]).value;
      if (value != this.getOption(typein[i])) {option[typein[i]] = value;}
    }
    for (id in checkbox) {
      value = this.control(checkbox[id]).checked;
      if (value != this.getOption(id)) {option[id] = value;}
    }
    var rows = [];
    for (id in option) {
      if (String(option[id]).match(/^ *[\-+]?(\d+(\.\d*)?|\.\d+)$/)) {
        rows.push('  '+id+': '+option[id]);
      } else if (typeof option[id] == 'boolean') {
        rows.push('  '+id+': '+(option[id] ? 1 : 0));
      } else {
        rows.push('  '+id+": '"+option[id]+"'");
      }
    }
    
    doc.write("<HEAD><TITLE>Mead/Median Evolver Data</TITLE></HEAD>\n");
    doc.write("<BODY>\n");
    doc.write("<PRE>\n");
    doc.write("&lt;script&gt;\n");
    doc.write("var sim = new jsMedian.Simulation({\n");
    doc.write(rows.join(",\n")+"\n");
    doc.write("});\n");
    doc.write("&lt;/script&gt;\n");
    doc.write("</PRE>");
    doc.write("</BODY>\n");
    doc.close();
  },
  
  /*********************************************/
  /*
   *  Animate the opening/closing drawer
   */
  pullDrawer: function () {
    var table = this.control("table"),
        drawer = this.control("drawer");
    if (!table.style.position) {
      drawer.style.height = table.offsetHeight + "px";
      table.style.position = "relative";
    }
    drawer.style.overflow = "hidden";
    if (this.drawerOpen) {
      this.pdata = {w: table.offsetWidth, h: table.offsetHeight,
                    W: parseInt(this.div().style.width,10), H: 0,
                    step: 1, steps: 15, open: 0};
    } else {
      this.pdata = {w: parseInt(this.div().style.width,10), h: 0,
                    W: table.offsetWidth, H: table.offsetHeight,
                    step: 1, steps: 15, open: 1};
    }
    setTimeout(this._pull,20);
  },

  pull: function () {
    var r = this.pdata.step / this.pdata.steps;
    r = 3*r*r-2*r*r*r; // use cubic to accelerate/decelerate
    var w = (1-r)*this.pdata.w + r*this.pdata.W;
    var h = (1-r)*this.pdata.h + r*this.pdata.H;
    var drawer = this.control("drawer"),
        pulldown = this.control("pulldown"),
        table = this.control("table");
    drawer.style.height = h + "px";
    drawer.style.width = pulldown.style.width = w + "px";
    table.style.top = (h-table.offsetHeight)+"px";
    this.pdata.step++;
    if (this.pdata.step <= this.pdata.steps) {
      setTimeout(this._pull,20);
    } else {
      if (this.pdata.open) {
        drawer.style.overflow = "";
        drawer.style.height = "auto"; 
        drawer.style.width = "";
        // MSIE display bug requires delay to set to "" (and "auto" above)
        setTimeout('jsMedian.Simulation.dataFor("'+this.id+'")' + 
                        '.control("drawer").style.height = ""',10);
      }
      this.drawerOpen = this.pdata.open;
      delete this.pdata;
    }
    pulldown.style.background = "";
  },
  
  /*********************************************/
  /*
   *  Find the simulation's canvas
   */
  div: function () {return jsMedian.Simulation.divFor(this);},

  /*********************************************/
  /*
   *  Compute the the length of a vector
   */
  norm: function (x,y) {return Math.sqrt(x*x+y*y);},
  
  /*********************************************/
  /*
   *  Add a new fixed point to the configuration
   */
  addPoint: function (x,y,event) {
    if (!this.getOption("allowNewPoints")) {return;}
    var dot = this.dots.add(x,y);
    dot.moveable = 1;
    jsMedian.active = dot.activate(event);
  },
  
  /*********************************************/
  /*
   *  Handle translating the configuration
   */  
  dragField: function (x,y,event) {this.drag.down.apply(this,arguments);},
  
  drag: {
    down: function (x,y,event) {
      if (!this.getOption("allowTransformations")) {return;}
      document.onmousemove = this.drag.move;
      document.onmouseup = this.drag.up;
      jsMedian.active = {sim: this, X: event.screenX, Y: event.screenY};
    },
  
    move: function (event) {
      event = jsMedian.getEvent(event);
      var active = jsMedian.active;
      if (active) {
        var dx = event.screenX - active.X; active.X = event.screenX;
        var dy = event.screenY - active.Y; active.Y = event.screenY;
        active.sim.dots.suspendDraw();
        active.sim.dots.moveby(dx,dy);
        active.sim.dots.resumeDraw();
        active.sim.point.moveby(dx,dy);
      }
      event.complete();
    },
  
    up: function (event) {
      event = jsMedian.getEvent(event);
      var active = jsMedian.active;
      if (active) {
        var dx = event.screenX - active.X;
        var dy = event.screenY - active.Y;
        document.onmouseup = null;
        document.onmousemove = null;
        delete jsMedian.active;
        active.sim.dots.moveby(dx,dy);
      }
      event.complete();
    }
  },
  
  /*********************************************/
  /*
   *  Handle scaling the configuration
   */
  scaleField: function (x,y,event) {this.scale.down.apply(this,arguments);},
  
  scale: {
    down: function (x,y,event) {
      this.stop();
      if (!this.getOption("allowTransformations")) {return;}
      document.onmousemove = this.scale.move;
      document.onmouseup = this.scale.up;
      var cX = this.point.x - x + event.screenX;
      var cY = this.point.y - y + event.screenY;
      var r = this.norm(event.screenX-cX,event.screenY-cY);
      jsMedian.active = {sim: this, X: cX, Y: cY, radius: r, last: 1};
    },
    
    move: function (event) {
      event = jsMedian.getEvent(event);
      var active = jsMedian.active;
      if (active) {
        var sim = active.sim;
        var r = sim.norm(event.screenX-active.X,event.screenY-active.Y)/active.radius;
        sim.dots.scale(r/active.last,sim.point.x,sim.point.y,!event.altKey);
        if (r !== 0) {active.last = r;}
      }
      event.complete();
    },
    
    up: function (event) {
      event = jsMedian.getEvent(event);
      var active = jsMedian.active;
      if (active) {
        var sim = active.sim;
        var r = sim.norm(event.screenX-active.X,event.screenY-active.Y)/active.radius;
        sim.dots.scale(r/active.last,sim.point.x,sim.point.y,!event.shiftKey);
        document.onmouseup = null;
        document.onmousemove = null;
        delete jsMedian.active;
      }
      event.complete();
    }
  },

  /*********************************************/
  /*
   *  Handle rotating the configuration
   */
  rotateField: function (x,y,event) {this.rotate.down.apply(this,arguments);},
  
  rotate: {
    down: function (x,y,event) {
      this.stop();
      if (!this.getOption("allowTransformations")) {return;}
      document.onmousemove = this.rotate.move;
      document.onmouseup = this.rotate.up;
      var cX = this.point.x - x + event.screenX;
      var cY = this.point.y - y + event.screenY;
      var r = Math.atan2(event.screenY-cY,event.screenX-cX);
      jsMedian.active = {sim: this, X: cX, Y: cY, angle: r};
    },
    
    move: function (event) {
      event = jsMedian.getEvent(event);
      var active = jsMedian.active;
      if (active) {
        var sim = active.sim;
        var r = Math.atan2(event.screenY-active.Y,event.screenX-active.X);
        sim.dots.rotate(r-active.angle,sim.point.x,sim.point.y);
        active.angle = r;
      }
      event.complete();
    },
    
    up: function (event) {
      event = jsMedian.getEvent(event);
      var active = jsMedian.active;
      if (active) {
        var sim = active.sim;
        var r = Math.atan2(event.screenY-active.Y,event.screenX-active.X);
        sim.dots.scale(r - active.angle,sim.point.x,sim.point.y);
        document.onmouseup = null;
        document.onmousemove = null;
        delete jsMedian.active;
      }
      event.complete();
    }
  },

  /*********************************************/
  /*
   *  Handle resizing the display canvas
   */
  resizeField: function (x,y,event) {this.resize.down.apply(this,arguments);},
  
  resize: {
    down: function (x,y,event) {
      document.onmousemove = this.resize.move;
      document.onmouseup = this.resize.up;
      var div = this.div();
      jsMedian.active = {
        sim: this, X: event.screenX, Y: event.screenY,
        w: parseInt(div.style.width,10),
        h: parseInt(div.style.height,10),
        r: 1
      };
      jsMedian.active.W = jsMedian.active.w;
      jsMedian.active.H = jsMedian.active.h;
    },
  
    move: function (event) {
      event = jsMedian.getEvent(event);
      var active = jsMedian.active;
      if (active) {
        var div = active.sim.div();
        var dx = event.screenX - active.X; active.X = event.screenX;
        var dy = event.screenY - active.Y; active.Y = event.screenY;
        active.w += dx; active.h += dy;
        var w = Math.max(active.sim.MINW,active.w),
            h = Math.max(active.sim.MINH,active.h);
        if (active.sim.MAXW) {w = Math.min(active.sim.MAXW,w);}
        if (active.sim.MAXH) {h = Math.min(active.sim.MAXH,h);}
        dx = (w - parseInt(div.style.width,10)) / 2;
        dy = (h - parseInt(div.style.height,10)) / 2;
        div.style.width  = w + "px";
        div.style.height = h + "px";
        if (!active.sim.drawerOpen) {
          active.sim.control("drawer").style.width =
          active.sim.control("pulldown").style.width = w + "px";
        }
        active.sim.dots.suspendDraw();
        active.sim.dots.moveby(dx,dy);
        active.sim.dots.resumeDraw();
        active.sim.point.moveby(dx,dy);

        var r, R; var P = active.sim.point;
        if (w*active.H < h*active.W)  {r = w / active.W;} else {r = h / active.H;}
        R = r/active.r; active.r = r;
        if (event.altKey) {
          active.sim.dots.scale(R,w/2,h/2,1);
          P.moveto(w/2+R*(P.x-w/2),h/2+R*(P.y-h/2));
        }
      }
      event.complete();
    },
  
    up: function (event) {
      event = jsMedian.getEvent(event);
      var active = jsMedian.active;
      if (active) {
        document.onmouseup = null;
        document.onmousemove = null;
        delete jsMedian.active;
      }
      event.complete();
    }
  },
  
  /*********************************************/
  /*
   *  Rescale the configuration to fit the canvas
   */
  normalize: function () {
    var size = this.point.SIZE();
    var div = this.div();
    var W = Math.max(size,div.offsetWidth - 4*size),
        H = Math.max(size,div.offsetHeight - 4*size);
    var bbox = this.dots.bbox();
    var r = 1, dx = 0, dy = 0;
    if (W*bbox.h < H*bbox.w) {
      if (bbox.w === 0) {dx = W / 2;} else {r = W / bbox.w;}
      dy = (H - r*bbox.h) / 2;
    } else {
      if (bbox.h === 0) {dy = H / 2;} else {r = H / bbox.h;}
      dx = (W - r*bbox.w) / 2;
    }
    this.dots.suspendDraw();
    this.dots.moveby(-bbox.x,-bbox.y);
    this.dots.scale(r,0,0,0);
    this.dots.moveby(dx+2*size,dy+2*size);
    this.point.moveto(r*(this.point.x-bbox.x)+dx+2*size,
                      r*(this.point.y-bbox.y)+dy+2*size);
    this.dots.resumeDraw();
    this.dots.drawLines();
    this.dots.showPoints();
  }
  
},{
  
  /*********************************************/
  /*
   *  Get unique identifier for each simulation
   */
  id: 0,
  ID: function () {this.id++; return "jsMedian_"+this.id;},

  /*********************************************/
  /*
   *  Get pointer to simulation data given a DOM element,
   *  element ID, or event
   */
  dataFor: function (div) {
    if (typeof div === 'string') {div = document.getElementById(div);}
    if (div.target) {div = div.target;}
      else if (div.srcElement) {div = div.srcElement;}
    while (div) {
      if (div.sData) {return div.sData;}
      if (div.sID) {div = document.getElementById(div.sID);}
        else {div = div.parentNode;}
    }
    return null;
  },

  /*********************************************/
  /*
   *  Get the container for the given simulation
   */
  divFor: function (sim) {return document.getElementById(sim.id);}
  
});

/***********************************************************************/
/***********************************************************************/
/*
 *  The list of fixed points
 */

jsMedian.DotList = jsMedian.Object.Subclass({
  defaults: {
    lineColor: "cyan",     // the line color
    shortColor: "#00FF00", // color for shortest line
    lineWidth: 2,          // the line width to use
    MINLINEWIDTH: 1,       // the minimum line width allowed
    SHORTESTFUZZ: 2        // the fuzziness for determining shortest line
  },
  
  first: null,      // linked list of points
  totalWeight: 0,   // the sum of weights of all points
  skipLines: 0,     // non-zero means drawing of lines is suspended
  
  /*********************************************/
  /*
   *  Link to the simulation and canvas ID's
   */
  Init: function (sID) {
    this.sID = sID; this.dID = sID+"_dots";
    this.setDefaults();
  },
  
  /*********************************************/
  /*
   *  Get values from user defaults
   */
  setDefaults: function () {
    var options = this.simulation().options;
    for (var id in this.defaults) {
      this[id] = (typeof options[id] !== 'undefined' ?
                    options[id]: this.defaults[id]);
    }
  },
  
  /*********************************************/
  /*
   *  Create a new point, add it to the linked list
   *  draw it, and restart
   */
  add: function (x,y,w) {
    var dot = new jsMedian.Dot(this.sID,this.div(),x,y,w);
    dot.next = this.first;
    if (this.first) {this.first.prev = dot;}
    this.first = dot;
    this.totalWeight++;
    this.drawLines();
    this.showPoints();
    var sim = this.simulation();
    if (!sim.running) {
      sim.enable("start","step","clear");
      sim.restart();
    }
    return dot;
  },
  
  /*********************************************/
  /*
   *  Remove a point from the list and the canvas
   */
  remove: function (dot) {
    this.totalWeight -= dot.weight;
    if (dot.next) {dot.next.prev = dot.prev;}
    if (dot.prev) {dot.prev.next = dot.next;} else {this.first = dot.next;}
    dot.next = dot.prev = null;
    dot.remove();
    this.drawLines();
    this.showPoints();
    if (!this.first) {this.simulation().disable("start","stop","step","clear");}
  },

  /*********************************************/
  /*
   *  Clear the list of points and the canvas
   */
  clear: function () {
    var dot = this.first;
    while (dot) {
      this.first = dot.next;
      dot.remove();
      dot = this.first;
    }
    this.totalWeight = 0;
    this.drawLines();
    this.showPoints();
  },
  
  /*********************************************/
  /*
   *  Compute the bounding box for the points
   *  in the list
   */
  bbox: function () {
    var simulation = this.simulation();
    var mx = simulation.point.x, my = simulation.point.y,
        Mx = simulation.point.x, My = simulation.point.y;
    var dot = this.first;
    while (dot) {
      if (dot.x < mx) {mx = dot.x;}
      if (dot.y < my) {my = dot.y;}
      if (dot.x > Mx) {Mx = dot.x;}
      if (dot.y > My) {My = dot.y;}
      dot = dot.next;
    }
    return {x: mx, y: my, w: Mx - mx, h: My - my};
  },
  
  /*********************************************/
  /*
   *  Translate all the points by a given amount
   */
  moveby: function (dx,dy) {
    if (dx === 0 && dy === 0) {return;}
    this.suspendDraw();
    var dot = this.first;
    while (dot) {
      dot.moveto(dot.x+dx,dot.y+dy);
      dot = dot.next;
    }
    this.resumeDraw();
    this.drawLines();
    this.showPoints();
  },
  
  /*********************************************/
  /*
   *  Scale all the points by a given amount
   *  toward the given point.  If "scale" is set
   *  the size changes, otherwise only the positions
   */
  scale: function (r,x,y,scale) {
    if (r === 1 || r === 0) {return;}
    var simulation = this.simulation();
    this.suspendDraw();
    var dot = this.first;
    while (dot) {
      if (scale) {dot.scale(r);}
      dot.moveto(x+r*(dot.x-x),y+r*(dot.y-y));
      dot = dot.next;
    }
    if (scale) {
      simulation.point.scale(r);
      this.lineWidth *= r;
      simulation.pointSize *= r;
      simulation.fontSize *= r;
    }
    this.resumeDraw();
    this.drawLines();
    this.showPoints();
  },
  
  /*********************************************/
  /*
   *  Rotate all the points by a given angle
   *  around a given point
   */
  rotate: function (a,x,y) {
    if (a === 0) {return;}
    var simulation = this.simulation();
    this.suspendDraw();
    var dot = this.first;
    while (dot) {
      var R = simulation.norm(dot.x-x,dot.y-y);
      var A = Math.atan2(dot.y-y,dot.x-x);
      dot.moveto(x+R*Math.cos(A+a),y+R*Math.sin(A+a));
      dot = dot.next;
    }
    this.resumeDraw();
    this.drawLines();
    this.showPoints();
  },
  
  /*********************************************/
  /*
   *  Suspend/resume drawing of lines
   *  (so when multiple points are adjusted
   *   the lines only need to be drawn once)
   */
  suspendDraw: function () {this.skipLines++;},
  resumeDraw: function () {if (this.skipLines) {this.skipLines--;}},
  
  /*********************************************/
  /*
   *   Draw lines from the center point to all
   *   the fixed points, coloring the shortest,
   *   if requested.  Time the process so that
   *   the delay between steps can be properly
   *   adjusted to keep the simulation rate close
   *   to constant.
   */
  drawLines: function () {
    var start = new Date().getTime();
    var simulation = this.simulation();
    if (!simulation.point) {return;}
    if (this.skipLines) {return;}
    var graph = simulation.graph;
    var dd = Math.floor(this.lineWidth/2);
    var P = simulation.point;
    var px = Math.floor(P.x + 0.5 - dd);
    var py = Math.floor(P.y + 0.5 - dd);
    var w = Math.max(this.MINLINEWIDTH,Math.floor(this.lineWidth+0.5));
    graph.clear(); graph.setStroke(w); graph.setColor(this.lineColor);
    var min = this.findMin(); if (!this.showShortest) {min = -1;}
    var dot = this.first;
    while (dot) {
      if (dot.distance < min) {
        dd = Math.max(1,dd);
        graph.setColor(this.shortColor); graph.setStroke(w+2*dd);
        graph.drawLine(px-dd,py-dd,dot.x-2*dd,dot.y-2*dd);
        if (dot.weight === 0) {
          graph.setColor(simulation.background); graph.setStroke(w);
          graph.drawLine(px,py,dot.x-dd,dot.y-dd);
        }
      } else if (dot.weight > 0) {
        graph.setColor(this.lineColor); graph.setStroke(w);
        graph.drawLine(px,py,dot.x-dd,dot.y-dd);
      }
      dot = dot.next;
    }
    graph.paint();
    var end = new Date().getTime();
    simulation.drawTime = end - start;
  },
  
  /*********************************************/
  /*
   *  Find the shortest distance of the center
   *  from any of the fixed points (and keep the
   *  computed distances for reference later).
   */
  findMin: function () {
    var sim = this.simulation();
    if (!this.showShortest && sim.model === 'Mean') {return -1;}
    var P = sim.point; var min = -1;
    var dot = this.first;
    while (dot) {
      dot.distance = sim.norm(P.x-dot.x,P.y-dot.y);
      if (min === -1 || dot.distance < min) {min = dot.distance;}
      dot = dot.next;
    }
    sim.shortest = min;
    return min + this.SHORTESTFUZZ;
  },
  
  /*********************************************/
  /*
   *  Display the point values in the type-in area
   */
  showPoints: function () {
    if (this.skipLines) {return;}
    var rows = [];
    var dot = this.first;
    while (dot) {
      var point = (dot.weight == 1 ? "" : dot.weight + " @ ")
        + "("+dot.x.toFixed(0)+", "+dot.y.toFixed(0)+")";
      if (dot.label) {
        point += " ="; if (dot.labelx !== null) {point += "\n ";}
        point += ' "'+dot.label.replace(/<br>/gi,"\n")+'"';
        if (dot.labelx !== null) {point += " @ ("+dot.labelx+','+dot.labely+')';}
      }
      rows[rows.length] = point;
      dot = dot.next;
    }
    this.simulation().control("points").value = rows.join("\n");
  },
  
  /*********************************************/
  /*
   *  Parse the type-in data and create points
   *  based on that
   */
  setPoints: function (string) {
    var parts = string.split(/"/); var i;
    for (i = 0; i < parts.length; i++) {
      if (i % 2 === 0) {parts[i] = parts[i].replace(/\n|\r| /g,"");}
                  else {parts[i] = parts[i].replace(/\n|\r/g,"<BR>");}
    }
    string = parts.join('"');
    if (string.match(/^((\d+@)?\(\d+,\d+\)(="[^"]*"(@\(-?\d+,-?\d+\))?)?)*$/)) {
      this.clear();
      this.suspendDraw();
      this.totalWeight = 0;
      var points = string.replace(/\)(="[^"]*"(?:@\(-?\d+,-?\d+\))?)?(\(|\d)/g,")$1;;$2").split(/;;/);
      for (i = points.length - 1; i >= 0; i--) {
        var result = points[i].match(/^(\d*)@?\((\d+),(\d+)\)(?:="([^"]*)"(?:@\((-?\d+),(-?\d+)\))?)?$/);
        var weight = (result[1] ? parseInt(result[1],10) : -1);
        var dot = this.add(parseInt(result[2],10),parseInt(result[3],10),weight);
        if (result[4]) {
          var x = (result[5] ? parseInt(result[5],10) : null),
              y = (result[6] ? parseInt(result[6],10) : null);
          dot.addLabel(result[4],x,y);
        }
        dot.hilite = 0; dot.draw();
        this.totalWeight += weight;
      }
      this.resumeDraw();
      this.drawLines();
    }
    this.showPoints();
  },
  
  /*********************************************/
  /*
   *  Get the canvas for points and lines,
   *  or the simulation data.  (Try to avoid pointer loops
   *  so MSIE will perform garbage collection properly.)
   */
  div: function () {return document.getElementById(this.dID);},
  simulation: function () {return jsMedian.Simulation.dataFor(this.sID);}
});


/***********************************************************************/
/***********************************************************************/
/*
 *  Implements the actions of a point on the canvas
 *  (subclassed for the median point later on)
 */
jsMedian.Dot = jsMedian.Object.Subclass({
  defaults: {
    MINSIZE: 5,         // minimum size of dot
    MAXWEIGHT: 15       // maximum weight to display (don't make huge dots)
  },
  flags: {
    showWeights: 1,         // show numbers for weights over 2
    pointColor: "blue",     // point color
    pointHColor: "#4488FF"  // highlight color (when mouse is over it)
  },
  
  prefix: "point",      // overridden in subclass
  
  hilite: 1,            // is highlighted?
  weight: 1,            // weight at this point
  distance: 0,          // distance to center (for shortest distance coloring)
  moveable: 0,          // controled by fixInitialPoints and allowNewPoints options

  /*********************************************/
  /*
   *  Create a new dot in a given canvas at a given location
   */
  Init: function (sID,sDIV,x,y,w) {
    this.id = jsMedian.Dot.ID(); this.sID = sID;
    this.setDefaults();
    this.color  = this.getOption(this.prefix+"Color");
    this.hcolor = this.getOption(this.prefix+"HColor");
    this.moveable = !this.getOption("fixInitialPoints");
    this.allowWeightChange = this.getOption("allowWeightChange");

    // a container for the dot and its hitbox
    var div = sDIV.appendChild(jsMedian.Element("div",{
      dot: this, id: this.id,
      style: {position: "absolute", cursor: "default"}
    }));

    // draw point here
    this.graph = new jsGraphics(div.appendChild(jsMedian.Element("div")));

    // a place for the weight label
    //  (MSIE messes up mouse events when the layer above is
    //   transparent so need to handle them here as well)
    div.appendChild(jsMedian.Element("div",{
      className: "jsMedianDotWeight",
      onmousedown: this.onmousedown,
      onmouseover: this.onmouseover,
      onmouseout:  this.onmouseout,
      ondblclick:  this.ondblclick
    }));

    // use this as hit box (since dot is made of lots
    // of small divs, it is not easy to trap the clicks)
    div.appendChild(jsMedian.Element("div",{
      onmousedown: this.onmousedown,
      onmouseover: this.onmouseover,
      onmouseout:  this.onmouseout,
      ondblclick:  this.ondblclick,
      style: {width: "100%", height: "100%", position: "absolute",
              left: "0px", top: "0px"}
    }));

    if (w >= 0) {
      this.weight = w;
      if (w > 1) {this.setWeightLabel(w); this.recenter();}
    }
    this.size = this.simulation().pointSize;
    div.style.width = div.style.height = this.SIZE() + "px";
    this.moveto(x,y);
    this.draw();
  },
  
  /*********************************************/
  /*
   *  Get values from user options or defaults
   */
  setDefaults: function () {
    var options = this.simulation().options;
    for (var id in this.defaults) {
      this[id] = (typeof options[id] !== 'undefined' ?
                    options[id]: this.defaults[id]);
    }
  },

  getOption: function (id) {
    var options = this.simulation().options;
    if (typeof options[id] !== 'undefined') {return options[id];}
    if (typeof this.flags[id] !== 'undefined') {return this.flags[id];}
    var flags = this.simulation().flags;
    if (typeof flags[id] !== 'undefined') {return flags[id];}
    return this.defaults[id];
  },

  /*********************************************/
  /*
   *  The (integer) size of the point, scaled by the weight,
   *  but clamped by the minimum size and maximum weight
   */
  SIZE: function () {
    var weight = Math.min(this.MAXWEIGHT,Math.max(1,this.weight));
    var size = this.size * Math.sqrt(weight);
    return Math.max(this.MINSIZE,Math.floor(size+0.5));
  },
  
  /*********************************************/
  /*
   *  Draw the point in its proper size and color
   */
  draw: function () {
    if (!this.graph) {return;}
    var color = this.hilite ? this.hcolor : this.color;
    var size = this.SIZE();
    var div = this.div();
    this.graph.clear();
    if (this.weight === 0) {
      div.style.width = div.style.height = (size+3)+"px";
      this.graph.setColor(this.simulation().background);
      this.graph.fillEllipse(1,1,size,size);
      this.graph.setColor(color);
      this.graph.setStroke(2);
      this.graph.drawEllipse(0,0,size-1,size-1);
    } else {
      div.style.width = div.style.height = size+"px";
      this.graph.setColor(color);
      this.graph.fillEllipse(0,0,size,size);
    }
    this.graph.paint();
  },
  
  /*********************************************/
  /*
   *  Create a label for a point and display it
   */
  addLabel: function (label,x,y) {
    this.label = label; this.labelx = x; this.labely = y;
    var div = this.div(); var size = this.SIZE();
    if (x === null) {x = 5}; if (y === null) {y = 0}
    if (x >= 0) {x += size;} y += Math.floor(size/2) - 10;
    label = label.replace(/<br>/gi,';;<br>;;').split(/;;/);
    this.simulation().createElements(this.div(),[
      "<div>",{
        className: "jsMedianLabel",
        id: this.labelID(), 
        style: {position:"absolute", left: x+"px", top: y+"px", whiteSpace: "nowrap"},
        onmousedown: function (event) {jsMedian.getEvent(event).complete()}
      },
      label
    ]);
  },

  /*********************************************/
  /*
   *  Recenter the point after a size change
   */
  recenter: function () {
    var div = this.div();
    var size = this.SIZE(), dd = size/2;
    if (this.weight === 0) {dd++;}
    div.style.left = Math.floor(this.x-dd+0.5)+"px";
    div.style.top  = Math.floor(this.y-dd+0.5)+"px";
    if (this.label) {
      var x = this.labelx, y = this.labely;
      if (x === null) {x = 5}; if (y === null) {y = 0}
      if (x >= 0) {x += size;}; y += Math.floor(size/2) - 10;
      div = this.labelDiv();
      div.style.left = x+"px"; div.style.top = y+"px";
    }
    this.centerWeight();
  },
  
  centerWeight: function () {
    if (this.weight > 1) {
      var label = this.weightSpan();
      label.style.fontSize = Math.floor(Math.max(25,this.simulation().fontSize))+"%";
      var size = this.SIZE();
      var dx = Math.floor((size - label.offsetWidth)/2),
          dy = Math.floor((size - label.offsetHeight)/2);
      label.style.left = dx+"px";
      label.style.top = dy+"px";
    }
  },
  
  /*********************************************/
  /*
   *  Remove the point from the canvas
   */
  remove: function () {
    var div = this.div();
    this.graph.clear(); delete this.graph;
    div.parentNode.removeChild(div);
  },
  
  /*********************************************/
  /*
   *  Move the point to the given location,
   *  redraw the lines, and show the new locations
   *  in the control panel
   */
  moveto: function (x,y) {
    this.x = x; this.y = y;
    this.recenter();
    var dots = this.simulation().dots;
    dots.drawLines(); dots.showPoints();
    this.showPosition();
  },
  
  /*********************************************/
  /*
   *  Move by a given delta
   */
  moveby: function (dx,dy) {this.moveto(this.x+dx,this.y+dy);},
  
  /*********************************************/
  /*
   *  Scale the size of the point
   */
  scale: function (r) {
    if (r === 1 || r === 0) {return;}
    this.size *= r;
    var div = this.div();
    div.style.width = div.style.height = this.SIZE() + "px";
    this.draw(); 
    this.recenter();
  },
  
  /*********************************************/
  /*
   *  Stubs to be overriden by subclass
   */
  restart: function () {if (jsMedian.active) {jsMedian.active.sim.restart();}},
  showPosition: function () {},
  
  /*********************************************/
  /*
   *  Adjust the weight of a point and redraw
   */
  setWeight: function (w) {
    if (!this.allowWeightChange) {return;}
    w = Math.max(0,w);
    var simulation = this.simulation();
    if (w === this.weight) {return;}
    if (w === 0 && this.moveable && !this.getOption("allowZeroWeights")) {
      simulation.dots.remove(this);
    } else {
      simulation.dots.totalWeight += w - this.weight;
      var drawLines = (w === 0 || this.weight === 0);
      this.weight = w;
      this.setWeightLabel(w);
      this.recenter();
      this.draw();
      if (drawLines) {simulation.dots.drawLines();}
    }
    simulation.dots.showPoints();
    simulation.restart();
  },
  
  /*********************************************/
  /*
   *  Adjust the weight label
   */
  setWeightLabel: function (w) {
    var label = this.weightSpan();
    if (label) {
      if (w > 1) {label.firstChild.nodeValue = w;}
      else {label.parentNode.removeChild(label);}        
    } else if (w > 1) {
      this.simulation().createElements(this.div().childNodes[1],[
        ["<span>",{style: {position: "relative"}, id: this.weightID()},[String(w)]]
      ]);
    }
  },
  
  /*********************************************/
  /*
   *  Start monitoring mouse events and
   *  return a structure for tracking this point
   *  in the canvas
   */
  activate: function (event) {
    if (!this.moveable) {return;}
    document.onmousemove = this.onmousemove;
    document.onmouseup = this.onmouseup;
    return {
      sim: this.simulation(), dot: this,
      X: event.screenX, Y: event.screenY,
      x: this.x, y: this.y
    };
  },
  
  /*********************************************/
  /*
   *  The mouse events for handling moving
   *  of the points, highlighting when the
   *  mouse moves over them, and double-clicking
   *  to delete.
   */
  onmousedown: function (event) {
    event = jsMedian.getEvent(event);
    var dot = jsMedian.Dot.dotFor(event);
    if (event.shiftKey) {dot.setWeight(dot.weight+1);}
    else if (event.altKey) {dot.setWeight(dot.weight-1);}
    else if (dot.moveable) {jsMedian.active = dot.activate(event);}
    else if (dot.allowWeightChange) {dot.setWeight(dot.weight+1);}
    event.complete();
  },
  
  onmouseup: function (event) {
    event = jsMedian.getEvent(event);
    var active = jsMedian.active;
    if (active) {
      var dx = event.screenX - active.X;
      var dy = event.screenY - active.Y;
      document.onmouseup = null;
      document.onmousemove = null;
      delete jsMedian.active;
      active.dot.moveto(active.x+dx, active.y+dy);
      active.dot.restart();
    }
    event.complete();
  },
  
  onmousemove: function (event) {
    event = jsMedian.getEvent(event);
    var active = jsMedian.active;
    if (active) {
      var dx = event.screenX - active.X;
      var dy = event.screenY - active.Y;
      active.dot.moveto(active.x+dx, active.y+dy);
      active.dot.restart();
    }
    event.complete();
  },
  
  onmouseover: function (event) {
    event = jsMedian.getEvent(event);
    if (jsMedian.active) {return;}
    var dot = jsMedian.Dot.dotFor(event);
    if (!dot.hilite && (dot.moveable || dot.allowWeightChange)) {
      dot.hilite = 1;
      dot.draw();
    }
    event.complete();
  },
  
  onmouseout: function (event) {
    event = jsMedian.getEvent(event);
    if (jsMedian.active) {return;}
    var dot = jsMedian.Dot.dotFor(event);
    if (dot.hilite) {
      dot.hilite = 0;
      dot.draw();
    }
    event.complete();
  },

  ondblclick: function (event) {
    event = jsMedian.getEvent(event);
    if (!event.shiftKey && !event.altKey) {
      var dot = jsMedian.Dot.dotFor(event);
      if (dot.moveable) {
        var sim = dot.simulation();
        sim.dots.remove(dot);
        sim.restart();
      }
    }
    event.complete();
  },
  
  /*********************************************/
  /*
   *  Find the convas or the owning simulation
   */
  div: function () {return jsMedian.Dot.divFor(this);},
  labelDiv: function () {return jsMedian.Dot.labelFor(this);},
  weightSpan: function () {return jsMedian.Dot.weightFor(this);},
  simulation: function () {return jsMedian.Simulation.dataFor(this.sID);},

  /*
   *  ID's for the sub-elements
   */
  labelID: function () {return this.id+"_label";},
  weightID: function () {return this.id+"_weight";}
},{

  /*********************************************/
  /*
   *  Get unique identifier for each point (could be
   *  simulation-specific, but no real need).
   */
  id: 0,
  ID: function () {
    this.id++;
    return "jsMedian_Dot_"+this.id;
  },
  
  /*********************************************/
  /*
   *  Get the data given a DOM element, name of an
   *  element, or event.
   */
  dotFor: function (div) {
    var target = div.target || div.srcElement; if (target) {div = target;}
    if (typeof div === "string") {div = document.getElementById(div);}
    while (div) {
      if (div.dot) {return div.dot;}
      div = div.parentNode;
    }
    return null;
  },

  /*********************************************/
  /*
   *  Find the container for the dot's graphics
   */
  divFor: function (dot) {return document.getElementById(dot.id);},
  labelFor: function(dot) {return document.getElementById(dot.id+"_label");},
  weightFor: function(dot) {return document.getElementById(dot.id+"_weight");}
});

/***********************************************************************/
/***********************************************************************/
/*
 *  Implements the special functions of the median/mean point.
 *  Subclass the general do to inherit most of its functionality
 */
jsMedian.Point = jsMedian.Dot.Subclass({
  defaults: {
    MINSIZE: 5,                   // minimum size of dot
    MAXWEIGHT: 15,                // maximum weight to display (don't make huge dots)
    MAXTRAIL: 10,                 // number of trail points to use
    TRAILCOLOR: [0xFF,0x80,0xC0], // darkest color for trail
    BGCOLOR: [0xFF,0xEE,0xF4]     // color to fade to
  },
  flags: {
    positionColor: "red",
    positionHColor: "orange"
  },
  
  prefix: "position",
  
  hilite: 0,                      // don't highlight (since it is drawn by default)

  /*********************************************/
  /*
   *  Create a point, but add a velocity and trail,
   *  and initialize them
   */
  Init: function () {
    this.SUPER(arguments).Init.apply(this,arguments);
    this.moveable = 1;
    this.velocity = {x: 0, y: 0};
    this.trail = [];
    this.showVelocity();
    this.setColors();
  },
  
  /*********************************************/
  /*
   *  Handle velocity changes when the point
   *  is moved, and do the autostart when
   *  it is dropped.
   */
  onmousedown: function (event) {
    event = jsMedian.getEvent(event);
    var dot = jsMedian.Dot.dotFor(event);
    dot.simulation().stop();
    dot.SUPER(arguments).onmousedown.call(this,event);
    dot.X = dot.x; dot.Y = dot.y; dot.time = event.time;
    dot.velocity.x = dot.velocity.y = 0; dot.timeD = 1;
    dot.showVelocity();
  },
  
  onmousemove: function (event) {
    event = jsMedian.getEvent(event);
    var dot = jsMedian.active.dot;
    dot.SUPER(arguments).onmousemove.call(this,event);
    dot.setVelocity(jsMedian.active.sim,event);
  },
  
  onmouseup: function (event) {
    event = jsMedian.getEvent(event);
    var dot = jsMedian.active.dot;
    var simulation = jsMedian.active.sim;
    dot.SUPER(arguments).onmouseup.call(this,event);
    if (event.shiftKey) {
      simulation.started = 0;
    } else if (simulation.control("autostart").checked) {
      if (event.time != dot.time) {
        dot.velocity.x *= dot.timeD / (event.time - dot.time);
        dot.velocity.y *= dot.timeD / (event.time - dot.time);
        dot.showVelocity();
      }
      dot.hilite = 0; dot.draw();
      simulation.start();
    }
  },
  
  /*********************************************/
  /*
   *  Override superclass methods
   */
  ondblclick: null,            // no deletion by double click
  restart: function () {},     // don't restart when moved
  setWeight: function () {},   // don't allow changes in weight
  
  /*********************************************/
  /*
   *  Set the velocity based on the distance and
   *  time taken during the move (doesn't work well
   *  in Opera and MSIE, since they don't time-stamp
   *  events and we add that ourselves later).
   */
  setVelocity: function (sim,event) {
    this.timeD = event.time - this.time;
    var k = sim.delay / this.timeD;
    this.velocity.x = k * (this.x - this.X);
    this.velocity.y = k * (this.y - this.Y);
    this.X = this.x; this.Y = this.y; this.time = event.time;
    this.showVelocity();
  },
  
  /*********************************************/
  /*
   *  Display the velocity and position in the
   *  type-in areas
   */
  showVelocity: function () {
    this.simulation().control("velocity").value =
      ("("+this.velocity.x.toFixed(2)+", "+this.velocity.y.toFixed(2)+")").
        replace(/-0\.00/g,"0.00");
  },
  
  showPosition: function () {
    this.simulation().control("position").value =
      "("+this.x.toFixed(1)+", "+this.y.toFixed(1)+")";
  },
  
  /*********************************************/
  /*
   *  Draw the position trail, dropping the oldest
   *  points when we get too many
   */
  trackTrail: function (x,y) {
    var simulation = this.simulation();
    if (simulation.control("showtrail").checked) {
      this.trail.unshift({x: this.x, y: this.y});
      if (this.trail.length > this.MAXTRAIL) {this.trail.pop();}
      var size = Math.max(this.MINSIZE-1,Math.floor(0.75*this.SIZE()));
      var dd = size/2;
      var trail = simulation.trail;
      trail.clear();
      for (var i = this.trail.length-1; i >= 0; i--) {
        trail.setColor(this.trailcolor[i]);
        var X = Math.floor(this.trail[i].x-dd+0.5),
            Y = Math.floor(this.trail[i].y-dd+0.5);
        trail.fillEllipse(X,Y,size,size);
      }
      trail.paint();
    }
    this.moveto(x,y);
    this.showVelocity();
  },
  
  /*********************************************/
  /*
   *  Clear the point trail
   */
  clearTrail: function () {
    this.trail = [];
    var trail = this.simulation().trail;
    trail.clear();
    trail.paint();
  },
  
  /*********************************************/
  /*
   *  Generate the fading colors for the trail
   */
  setColors: function () {
    this.trailcolor = [];
    var r = this.TRAILCOLOR[0], g = this.TRAILCOLOR[1], b = this.TRAILCOLOR[2];
    var R = this.BGCOLOR[0],    G = this.BGCOLOR[1],    B = this.BGCOLOR[2];
    for (var i = 0; i < this.MAXTRAIL; i++) {
      var t = i/this.MAXTRAIL;
      this.trailcolor.push(this.RGB((1-t)*r+t*R,(1-t)*g+t*G,(1-t)*b+t*B));
    }
  },
  
  /*********************************************/
  /*
   *  Produce RGB values in hex
   */
  hexDigit: "0123456789ABCDEF".split(''),
  RGB: function (r,g,b) {return "#"+this.Hex(r)+this.Hex(g)+this.Hex(b);},
  Hex: function (n) {return this.hexDigit[n >> 4] + this.hexDigit[n & 0xF];}
});

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