/* ----------------------------------------------------------------------------

       This confidential and proprietary software  may be used only as
       authorized by a licensing agreement from Riverside Machines Ltd.
  
                 (C) COPYRIGHT 2011 Riverside Machines Ltd.
                            ALL RIGHTS RESERVED
  
    The entire notice above must be reproduced on all authorized
    copies and any such reproduction must be pursuant to a licensing
    agreement from Riverside Machines Ltd.

 * ------------------------------------------------------------------------- */
Raphael.fn.polyline = function (x,y) { 
    var poly = ['M',x,y,'L']; 
    for (var i=2;i<arguments.length;i++) { 
            poly.push(arguments[i]); 
    } 
    return this.path(poly.join(' ')); 
} 

var title, yaxis, xaxis, comment, NorfolkPopn, NorfolkTotal, NorfolkRate, NorfolkRateStr, HEScode, surgeryList, PoissonPracticeSize, PoissonExpectedRate, PoissonLimitsLo2s, PoissonLimitsHi2s, PoissonLimitsLo3s, PoissonLimitsHi3s, ourSurgeries;

var paper;
var svgns = "http://www.w3.org/2000/svg";

// select the consortium view if this is set, and the surgery view otherwise.
// this needs to be set via the php/whatever - it's currently hard-wired to 0
var consortiumView = 1;

// ----------------------------------------------------------------------------
// the scaling algorithm fills in stringsY and nyBlocks
var stringsY; // = [24, 22, 20, 18, 16, 14, 12, 10, 8, 6, 4, 2, 0];
var nYblocks; // = 12;

// currently fixed, because we know all the surgery sizes
var stringsX = [
    1000,  3000,  5000, 7000, 9000, 11000, 13000, 15000, 17000,
   19000, 21000, 23000];
var nXblocks = 11;
var xBlockPx = 35;         // pixel width of each block
var yBlockPx = 35;         // pixel height of each block
// ----------------------------------------------------------------------------

// ----------------------------------------------------------------------------
// general viewport setup. the total canvas size is:
// x: GRID_LEFT  + (nXblocks * xBlockPx) + GRID_RIGHT = 480
// y: GRID_ABOVE + (nYblocks * yBlockPx) + GRID_BELOW = 320
var GRID_ABOVE   = 60;      // top strip height
var GRID_LEFT    = 60;      // left strip width
var GRID_BELOW   = 94;      // bottom strip height
var GRID_RIGHT   = 160;     // rigth strip width
var X_LGND_OFFS_DOWN  = 14; // x legend text offset below the x axis
var X_LGND_OFFS_RIGHT = 3;  // offset to the right of the corresponding y line
var Y_LGND_OFFS_LEFT  = 12; // offset to the left of the grid (right-justify)
var Y_LGND_OFFS_DOWN  = 4;  // offset below the corresponding x line

// the pixel extent of the grid lines beyond the axis ('cross marks');
// may be zero
var LineOverHangLeft = 4;
var LineOverHangDown = 4;

// the main legend boxes and text:
var LGND_STEP_VERTICAL  = 19;  // vertical pixel offset per surgery
var LGND_BOX_OFFS_RIGHT = 12;  // box offsets to the right of the grid
var LGND_BOX_OFFS_DOWN  = 0;   // first box offset below the grid
var LGND_BOX_SIZE       = 12;  // legend box size, both x and y
var LGND_TXT_OFFS_RIGHT = 5;   // text offset from right edge of box
var LGND_TXT_OFFS_DOWN  = 10;  // text offset below top of box

// the poisson legend:
var LGND_PSN_OFFS1_DOWN = 38;
var LGND_PSN_OFFS2_DOWN = 57;

// put dots on the X and Y axes if set
var DO_AXIS_DOTS = 1;

var font_title   = "Tahoma";
var font_legend  = "Tahoma";
var font_numbers = "Arial";
var font_popup   = "Arial";

var DOT_RADIUS = 3.0;
var DOT_STROKE_WIDTH = 2.5;
// ----------------------------------------------------------------------------

// colours 0-4 are used for the first 5 legend surgeries; everything after
// that gets 5
var NCOLOURS = 6;
var colours  = [
    "#E810D6", "#7110E8", "#109DE8", "#14E810", "#E8AB10", "#aaaaaa"];

var colourPoisson2S = "#FFBF00";    // traffic amber, from Wikipedia (!)
var colourPoisson3S = "#E32636";    // alizarin crimson
var colourAverage   = "#52A9D7";    // turquoise

// consortium colours, if the consortium view is selected
var colourNorth   = "#FF0000";      // red
var colourSouth   = "#FF00FF";      // mauve
var colourWest    = "#00FF00";      // green
var colourMid     = "#000000";      // black
var colourNorwich = "#0000FF";      // blue
var colourUnknown = "#AAAAAA";

//" the" margin to leave at each end of the display ranges; in other
// words, the blank graph area on the left, right, top, and bottom
var RANGE_MARGIN = 0.1;   // ie. 10%

// pixel centre points for each surgery
var surgeryPix;

// offsets in surgeryPix:
var S_NAME = 0;           // the id of the dot for this surgery
var S_X    = 1;           // surgery dot x
var S_Y    = 2;           // surgery dot y

// the invisible bounding box size aroung each dot. the sides are odd
// numbers, to get a centre pixel
var bBox = 11;

/* these nodes are created on mouseover, and deleted on mouseout.
 * however, it's possible to get a second mouseover before mouseout,
 * so we need to protect access with the predicate. the 2 nodes are
 * only valid when inMSR is set. */
var inMSR = 0;
var dotClone, dotHollow;

function svgInit(evt, var01,var02,var03,var04,var05,var06,var07,var08,var09,var10,var11,var12,var13,var14,var15,var16,var17) {
  title = var01;
	yaxis = var02;
	xaxis = var03;
	comment = var04;
	NorfolkPopn = var05;
	NorfolkTotal = var06;
	NorfolkRate = var07;
	NorfolkRateStr = var08;
	HEScode = var09;
	surgeryList = var10;
	PoissonPracticeSize = var11;
	PoissonExpectedRate = var12;
	PoissonLimitsLo2s = var13;
	PoissonLimitsHi2s = var14;
	PoissonLimitsLo3s = var15;
	PoissonLimitsHi3s = var16;
	ourSurgeries = var17;
if(window.paper == null)
//    paper = evt.target.ownerDocument;
    paper = evt;
  try {
    initialisation(evt);
  } catch(e) {
    alert(e.name + ": " + e.message);
    // skip to another page?
    // document.location.href="http://www.w3schools.com/";
  }
}

/**
 * the javascript 2D array stuff is pretty naff. assigning 'pos'
 * directly to 'surgeryPix[i]' doesn't (in Chrome, at least) create a
 * new 2-element array at 'surgeryPix[i]'. similarly, even if a new
 * array is explicitly created, assigning 'pos' directly to it doesn't
 * work (in Chrome, at least).
 *
 * The centre-points are rounded (to nearest) to ensure that we can
 * always precisely determine which surgery a mouse event refers to.
 */
function initialisation(evt) {
  scaleY();                      // can't do anything till this is done
  set_chart_size();              // svg element width and height

  // draw the grid as the lowest layer, and then the title text
  draw_grid();
  draw_title_text();
  draw_average_line();

  // this clipping path applies only to the Poisson curves; the dots should
  // always fit in
//  set_chart_clip_path();

  draw_poisson(PoissonLimitsLo2s, colourPoisson2S);
  draw_poisson(PoissonLimitsHi2s, colourPoisson2S);
  draw_poisson(PoissonLimitsLo3s, colourPoisson3S);
  draw_poisson(PoissonLimitsHi3s, colourPoisson3S);

  // convert the data points for all surgeries into integral pixel
  // values
  var pos = [0,0];
  surgeryPix = new Array(surgeryList.length);
  for(var i=0; i<surgeryList.length; ++i) {
    pos[0] = getPixelX(surgeryList[i][1]);
    pos[1] = getPixelY(surgeryList[i][3]);
    surgeryPix[i] = new Array(3);
    surgeryPix[i][0] = surgeryList[i][0];
    surgeryPix[i][1] = Math.round(pos[0]);
    surgeryPix[i][2] = Math.round(pos[1]);
  }

  // fill in the indexes for our surgeries
  var found;
  for(var i=0; i<ourSurgeries.length; ++i) {
     found = false;
     for(var j=0; j<surgeryList.length; ++j) {
        if(surgeryList[j][0] == ourSurgeries[i][1]) {
           ourSurgeries[i][0] = j;
           found = true;
           break;
        }
     }
     init_assert(found, 3);
  }

  // plot all the other surgeries as a single group, and then our surgeries on
  // top
  plot_other_surgeries("OtherSurgeries");
  if(!consortiumView)
      plot_DHC_surgeries("DHC");

  draw_legend();
} // initialisation()

/**
 * Scale the Y axis. We want approximately 12 horizontal grid lines (the
 * number of vertical grid lines is currently fixed at 12). This fixes
 * 'nYblocks' and 'stringsY'. We also want the number of vertical pixels
 * between horizontal grid lines ('yBlockPx') to be approximately 35. Keeping
 * close to these numbers will give a square display.
 */
function scaleY() {
   var i, rate, range;
   var minRate, maxRate;
   var maxVal = surgeryList[0][3];
   var minVal = surgeryList[0][3];
   for(var i=1; i<surgeryList.length; ++i) {
      rate = surgeryList[i][3];
      if(rate > maxVal)
         maxVal = rate;
      if(rate < minVal)
         minVal = rate;
   }

   minRate = minVal;             // save for checking below
   maxRate = maxVal;
   maxVal  = Math.ceil(maxVal);  // round up
   minVal  = Math.floor(minVal); // round down
   if(maxVal >= 5 * minVal)      // no point having small numbers at the bottom
      minVal = 0;
   range  = maxVal - minVal;

   // make some guesses (3) at a step size, and select the best one
   var guesses = new Array(3);
   var nblocks, step, nsteps;
   for(i=0; i<3; ++i) {
      nblocks = 10+i;            // try 10, 11, 12 blocks (11-13 lines)
      step    = range / nblocks;
      step    = Math.round(step);
      // solve ((minVal + step*nsteps) >= maxVal) for nsteps
      nsteps  = Math.ceil((maxVal - minVal)/step);
      guesses[i] = new Array(2);
      guesses[i][0] = step;
      guesses[i][1] = nsteps;
   }

   // now find the guess with the closest to 11 blocks/12 lines
   var best_guess = 0;
   var best_diff  = Math.abs(guesses[best_guess][1] - 11);
   var this_diff;
   for(i=1; i<3; ++i) {
      this_diff = Math.abs(guesses[i][1] - 11);
      if(this_diff < best_diff) {
         best_guess = i;
         best_diff  = this_diff;
      }
   }

   step     = guesses[best_guess][0];
   nYblocks = guesses[best_guess][1];
   stringsY = new Array(nYblocks + 1);

   for(i=0; i<=nYblocks; i++)
      stringsY[i] = minVal + (nYblocks-i) * step;
   init_assert((stringsY[0] >= maxRate),        0);
   init_assert((stringsY[nYblocks] >= 0),       1);
   init_assert((stringsY[nYblocks] <= minRate), 2);

   if(typeof console != 'undefined' && console != null) {
      console.debug("minRate, maxRate: (%d, %d)\n", minRate, maxRate);
      console.debug("guess A: (%d, %d)\n", guesses[0][0], guesses[0][1]);
      console.debug("guess B: (%d, %d)\n", guesses[1][0], guesses[1][1]);
      console.debug("guess C: (%d, %d)\n", guesses[2][0], guesses[2][1]);
   }
} // scaleY()

/**
 * Draw the legend down the right-hand-side of the chart.
 */
function draw_legend() {
  var txt, x, y;

  // the text template
    txt = paper.text(x, y, "").initZoom().setAttr({
        "font-family": font_legend,
        "font-size": 12,
        "stroke": "none",
        "stroke-width": 0,
        "fill-opacity": 1,
        "stroke-opacity": 1,
        "stroke-dasharray": 0,
        "fill": "#222222"
    });
  if(!consortiumView)
      draw_surgery_legend(txt);

  // draw everything else, starting at the bottom
  x = GRID_LEFT  + LGND_BOX_OFFS_RIGHT + (nXblocks * xBlockPx);
  y = GRID_ABOVE + (nYblocks * yBlockPx);
  draw_legend_box(x, y, txt, colourPoisson3S, "Poisson limit (3\u03c3)");

  y -= LGND_STEP_VERTICAL;
  draw_legend_box(x, y, txt, colourPoisson2S, "Poisson limit (2\u03c3)");

  y -= LGND_STEP_VERTICAL;
  draw_legend_box(x, y, txt, colourAverage,   NorfolkRateStr);

  if(consortiumView) {
      y -= LGND_STEP_VERTICAL;
      draw_legend_dot(x, y, txt, colourUnknown,   "Unknown CCG");

      y -= LGND_STEP_VERTICAL;
      draw_legend_dot(x, y, txt, colourNorwich,   "Norwich CCG");

      y -= LGND_STEP_VERTICAL;
      draw_legend_dot(x, y, txt, colourWest,      "West Norfolk CCG");

      y -= LGND_STEP_VERTICAL;
      draw_legend_dot(x, y, txt, colourMid,       "Mid Norfolk CCG");

      y -= LGND_STEP_VERTICAL;
      draw_legend_dot(x, y, txt, colourSouth,     "South Norfolk CCG");

      y -= LGND_STEP_VERTICAL;
      draw_legend_dot(x, y, txt, colourNorth,     "North Norfolk CCG");
  }
} // draw_legend()

/**
 * Draw the legend for the Dereham surgeries. This is a set of coloured boxes
 * (the colour corresponds to the surgery dot colour), with mouseover, and the
 * surgery name.
 *
 * @param text The text template
 */
function draw_surgery_legend(txt) {
   var obj, str, colour, x, y, index;

  // get the top left co-ordinate of the first legend box
  x = GRID_LEFT  + LGND_BOX_OFFS_RIGHT + (nXblocks * xBlockPx);
  y = GRID_ABOVE + LGND_BOX_OFFS_DOWN;

  // this is the template legend box
  var lbox = paper.rect(x , y, LGND_BOX_SIZE, LGND_BOX_SIZE).initZoom().setAttr({
        "stroke": "none",
        "stroke-width": 0,
        "fill-opacity": 1,
        "stroke-opacity": 1,
        "stroke-dasharray": 0,
    });
  
    var grp = paper.set();
    
  for(var i=0; i<ourSurgeries.length; ++i) {
    index  = ourSurgeries[i][0];
    colour = (i < NCOLOURS-1)? colours[i] : colours[NCOLOURS-1];
    obj = lbox.clone().initZoom().setAttr({"y": y, "fill": colour, "onmouseover": "legend_on (evt,"+index+")", "onmouseout": "legend_off(evt,"+index+")"});
    grp.push(obj);
    y += LGND_STEP_VERTICAL;
  }

  // get the bottom-left co-ordinate of the text for the first box
  x += (LGND_BOX_SIZE + LGND_TXT_OFFS_RIGHT);
  y  = GRID_ABOVE + LGND_BOX_OFFS_DOWN + LGND_TXT_OFFS_DOWN;

  // add the short-foem surgery name to the right of the box
  for(var i=0; i<ourSurgeries.length; ++i) {
    obj = txt.clone().initZoom().setAttr({ "x": x, "y": y, "text": ourSurgeries[i][2]});
    grp.push(obj);
    y += LGND_STEP_VERTICAL;
  }
} // draw_surgery_legend()

/**
 * Draw a short coloured horizontal line in the legend area. This is line
 * before the average and Poisson text. The line is across the middle of a
 * square box of side length LGND_BOX_SIZE.
 */
function draw_legend_box(x, y, txt, colour, str) {
   var poly;
   var point1 = [x,                 y - LGND_BOX_SIZE/2 +2];
   var point2 = [x + LGND_BOX_SIZE, y - LGND_BOX_SIZE/2 +2];
   var points = 
      " " + point1[0] + "," + point1[1] +
      " " + point2[0] + "," + point2[1];

   poly = paper.polyline(points).initZoom().setAttr({
        "stroke": colour,
        "stroke-width": 1.5,
        "fill-opacity": 0,
        "stroke-opacity": 0.75,
        "stroke-dasharray": 0,
        "fill": "none"
    });

    var grp = paper.set();
   // now append the legend to the right
   x  += (LGND_BOX_SIZE + LGND_TXT_OFFS_RIGHT);
   obj = txt.clone().initZoom().setAttr({ "x": x, "y": y, "text": str});
   grp.push(obj);
}  // draw_legend_box()

/**
 * Draw a coloured dot in the legend area. This is the dot before the
 * consortium name. The dot is in the middle of a square box of side length
 * LGND_BOX_SIZE.
 */
function draw_legend_dot(x, y, txt, colour, str) {
   var shape;

    shape = paper.circle(x + LGND_BOX_SIZE/2, y - LGND_BOX_SIZE/2 + 1, 2).initZoom().setAttr({
        "stroke": colour,
        "stroke-width": 1.3,
        "fill-opacity": 1,
        "stroke-opacity": 1,
        "stroke-dasharray": 0,
        "fill": colour,
        "pointer-events": "none"
    });

    var grp = paper.set();
// now append the legend to the right
   x  += (LGND_BOX_SIZE + LGND_TXT_OFFS_RIGHT);
   obj = txt.clone().initZoom().setAttr({ "x": x, "y": y, "text": str});
   grp.push(obj);
}  // draw_legend_dot()

/**
 * Set the width and height of the svg element, and fill the entire
 * canvas with white. I'm not quite sure what the half-pix offset is
 * in aid of...
 */
function set_chart_size() {
  var obj, width, height;
  width  = GRID_LEFT  + (nXblocks * xBlockPx) + GRID_RIGHT;
  height = GRID_ABOVE + (nYblocks * yBlockPx) + GRID_BELOW;

    obj = paper.rect(GRID_LEFT+1, GRID_ABOVE+1, nXblocks*xBlockPx-1, nYblocks*yBlockPx-1).initZoom().setAttr({
		"id": "chartClip",
		"stroke": "none",
		"stroke-width": 0,
		"fill-opacity": 1,
		"stroke-opacity": 1,
		"stroke-dasharray": 0,
		"fill": "none"
    });
    
    obj = paper.rect(0.5, 0.5, width - 1, height - 1).initZoom().setAttr({
		"stroke":           "none",
		"stroke-width":     0,
		"fill-opacity":     1,
		"stroke-opacity":   1,
		"stroke-dasharray": 0,
		"fill":             "#ffffff"
	});
}

/**
 * Plot all surgeries if 'consortiumView' is true, and only the non-Dereham
 * surgeries otherwise.
 */
function plot_other_surgeries(group_name) {
   var i, j, skip, group, colour;

   group = paper.set().attr({ "id": group_name});

   for(i=0; i<surgeryPix.length; ++i) {
      if(!consortiumView) {
	  // skip the DHC surgeries; they're plottted by 'plot_DHC_surgeries'
	  skip = false;
	  for(j=0; j<ourSurgeries.length; j++) {
              if(ourSurgeries[j][0] == i) {
		  skip = true;
		  break;
              }
	  }
	  if(skip)
              continue;
      }

      // find this surgery's consortium and colour
      if(consortiumView) {
	  switch(surgeryList[i][6]) {
	  case "North"   : colour = colourNorth;   break;
	  case "South"   : colour = colourSouth;   break;
	  case "Mid"     : colour = colourMid;     break;
	  case "West"    : colour = colourWest;    break;
	  case "Norwich" : colour = colourNorwich; break;
	  case "Unknown" : colour = colourUnknown; break;
	  default        : init_assert(false, 4);
	  }
      } else
	  colour = colours[5];

      plot_surgery(group, i, surgeryPix[i], 7, 1.3, 2.0, colour);
   }
}  // plot_other_surgeries()

function draw_average_line() {
   var poly;
   var y      = getPixelY(NorfolkRate);
   var point1 = [GRID_LEFT, y];
   var point2 = [GRID_LEFT + nXblocks*xBlockPx, y];
   var points = 
      " " + point1[0] + "," + point1[1] +
      " " + point2[0] + "," + point2[1];

   poly = paper.polyline(points).initZoom().setAttr({
        "stroke": colourAverage,
        "stroke-width": 1.5,
        "fill-opacity": 0,
        "stroke-opacity": 0.75,
        "stroke-dasharray": 0,
        "fill": "none",
        "clip-path": "url(#chartClip)"
    });
}  // draw_average_line()

function draw_poisson(limit, colour) {
   var group, clip_path, poly, x, y;
   var points = " ";
   var npoints = PoissonPracticeSize.length;

   for(var i=0; i<npoints; i++) {
      x       = getPixelX(PoissonPracticeSize[i]);
      y       = getPixelY(limit[i]);
      points += " " + x + "," + y;
   }

   poly = paper.polyline(points).initZoom().setAttr({
        "stroke": colour,
        "stroke-width": 1.5,
        "fill-opacity": 0,
        "stroke-opacity": 0.75,
        "stroke-dasharray": 0,
        "shape-rendering": "crisp-edges",
        "fill": "none",
        "clip-path": "url(#chartClip)"
    });
}  // draw_poisson()

/**
 * Plot the DHC surgeries.
 */
function plot_DHC_surgeries(group_name) {
   var i, index;

   group = paper.createElementNS(svgns, "g");
   group.setAttributeNS(null, "id", group_name);
   for(i=0; i<ourSurgeries.length; i++) {
      index = ourSurgeries[i][0];
      plot_surgery(
         group, index, surgeryPix[index], bBox, DOT_STROKE_WIDTH,
         DOT_RADIUS, colours[i]);
   }
   paper.documentElement.appendChild(group);
}  // plot_DHC_surgeries()

/**
 * Create and return a group containing a dot, and a surrounding rectangle.
 * The inner dot represents a surgery, and is given an id with the surgery
 * identifier. Can't get attirbute inheritance to work, so have passed
 * in various attributes.
 */
function plot_surgery(
   group, index, surgery, bbox, stroke_width, dot_radius, colour)
{
  var x   = surgery[1];
  var y   = surgery[2];
  var shape;

  // draw the bounding box
  shape = paper.rect(x-(bbox/2), y-(bbox/2), bbox, bbox).initZoom().setAttr({
        "stroke": "none",
        "stroke-width": 0,
        "fill-opacity": 0,
        "stroke-opacity": 0,
        "stroke-dasharray": 0,
        "fill": "#ffffff",
        "onmouseover": "popup_on (evt,"+index+")",
        "onmouseout": "popup_off(evt,"+index+")"
    });
  group.push(shape);

  // and the inner dot. we need to save an id for the dot so that we
  // can locate it from the bounding box
  shape = paper.circle(x, y, dot_radius).initZoom().setAttr({
    "stroke": colour,
    "stroke-width": stroke_width,
    "fill-opacity": 1,
    "stroke-opacity": 1,
    "stroke-dasharray": 0,
    "fill": colour,
    "pointer-events": "none",
    "id": surgery[0]
    });   // <-- the magic
  group.push(shape);
} // plot_surgery()

/**
 * Get the x pixel value corresponding to population 'popn'. There's a
 * tricky problem with getting the right-hand edge right. If we, for
 * example, 5 x blocks, each of 70 pixels, and GRID_LEFT is 90, the
 * right-hand vertical line will appear at x position 90+5*70 = 440.
 * However, this line doesn't correspond to the right-most population
 * value: it corresponds to the *start* of the *next* block. It's
 * actually drawn over the *next* pixel. However, this is actually
 * correct, sincwe we want to plot two Kpopulation values,
 * inclusively: for example, 0 patients, and 12,000 patients. To
 * handle this, the grid width calculation below must add 1 to the
 * grid width to account for this last pixel.
 */
function getPixelX(popn) {
  var minVal = stringsX[0];
  var maxVal = stringsX[nXblocks];

  // get the 0-based indexes of the leftmost and rightmost pixels in
  // the grid
  var minPx = GRID_LEFT;
  var maxPx = GRID_LEFT + (nXblocks * xBlockPx);  // inclusive

  var ratio = (popn - minVal) / (maxVal - minVal);
  return Math.round(minPx + ratio * (maxPx - minPx + 1));
}

/**
 * Get the y pixel value corresponding to rate 'rate'.
 */
function getPixelY(rate) {
  var minVal = stringsY[nYblocks];
  var maxVal = stringsY[0];
  if((rate < minVal) || (rate > maxVal))
     if(typeof console != 'undefined' && console != null)
        console.warn(
           "rate: %f; minVal: %f; maxVal: %f\n", rate, minVal, maxVal);

  var minPx = GRID_ABOVE + (nYblocks * yBlockPx);
  var maxPx = GRID_ABOVE;

  var ratio = (rate - minVal) / (maxVal - minVal);
  return Math.round(minPx - ratio * (minPx - maxPx));
}

/**
 * Produce a popup when we get a mouseover on a surgery dot. 'surgery'
 * is the index of the required surgery.
 *
 * The orientation of the popup (speech bubble upright or upside down)
 * is set here. The current scheme is that we get an upright bubble
 * when approaching from below, and vice-versa.
 */
function popup_on(evt, surgery)
{
  if(inMSR) {
     if(typeof console != 'undefined' && console != null)
        console.warn("Warning: popup_on re-entry\n");
    return;               // ignore this mouseover
  }
  inMSR = 1;

  var obj, colour, cx, cy;
  var offsetx, offsety;   // offset required to fix popup pointer position
  var upright;

  // if 'surgery' is DHC, it gets the pre-assigned colour, otherwise it gets
  // the default colour
  var index = convertSurgeryIndexToDHCIndex(surgery);
  if(index == -1)
     colour = colours[NCOLOURS-1];
  else
     colour = colours[index];
  cx     = surgeryPix[surgery][S_X];
  cy     = surgeryPix[surgery][S_Y];

  upright = (evt.clientY >= cy);

  if(upright) {
    offsetx = cx - 24;
    offsety = cy - 55;
  } else {
    offsetx = cx - 24;
    offsety = cy;
  }

  // draw the popup; the '24' is the x offset from the pop-up origin
  // to the tip of the pointer
  draw_popup(surgeryList[surgery], offsetx, offsety, upright);

  // clone the centre dot (*not* the target), save it and delete it,
  // and replace it with a hollowed-out dot
  obj = document.getElementById(surgeryPix[surgery][S_NAME]);
  dotClone = obj.clone();               // save for popup_off
  obj.parentNode.removeChild(obj);

  obj = paper.createElementNS(svgns,     "circle");
  obj.setAttributeNS(null, "cx",               cx);
  obj.setAttributeNS(null, "cy",               cy);
  obj.setAttributeNS(null, "r",                DOT_RADIUS);
  obj.setAttributeNS(null, "stroke",           colour);
  obj.setAttributeNS(null, "stroke-width",     DOT_STROKE_WIDTH);
  obj.setAttributeNS(null, "fill-opacity",     1);
  obj.setAttributeNS(null, "stroke-opacity",   1);
  obj.setAttributeNS(null, "stroke-dasharray", 0);
  obj.setAttributeNS(null, "fill",             "#ffffff");
  obj.setAttributeNS(null, "pointer-events",   "none");
  paper.documentElement.appendChild(obj);

  dotHollow = obj;   // save so that we can delete it
} // popup_on()

/**
 * Delete a surgery popup when we get a mouseout. We have to delete
 * all the popup elements, and the hollow dot, and restore the old
 * complete dot in 'dotClone'.
 */
function popup_off(evt)
{
  if(!inMSR) {
     if(typeof console != 'undefined' && console != null)
        console.warn("Warning: popup_off re-entry\n");
     return;               // ignore this mouseout
  }
  inMSR = 0;

  var obj;
  for(var i=1; (obj = document.getElementById("pbox"+i)) != null; ++i)
    obj.parentNode.removeChild(obj);

  dotHollow.parentNode.removeChild(dotHollow);
  paper.documentElement.appendChild(dotClone);
} // popup_off()

/**
 * mouseover function for the legend boxes.
 */
function legend_on(evt, surgery) {
  if(inMSR) {
    if(typeof console != 'undefined' && console != null)
       console.warn("Warning: legend_on re-entry\n");
    return;               // ignore this mouseover
  }
  inMSR = 1;

  var obj, colour, cx, cy;

  obj = document.getElementById(surgeryPix[surgery][S_NAME]);
  cx  = surgeryPix[surgery][S_X];
  cy  = surgeryPix[surgery][S_Y];

  dotClone = obj.clone();
  obj.parentNode.removeChild(obj);

  /* convert the main surgery index into an index into 'ourSurgeries' and
   * 'colours'. this should assert on failure, but I've currently only got a
   * throw back to the init call */
  var index = convertSurgeryIndexToDHCIndex(surgery);

  colour = (index < NCOLOURS-1)? colours[index] : colours[NCOLOURS-1];
  obj = paper.createElementNS(svgns, "circle");
  obj.setAttributeNS(null, "cx",               cx);
  obj.setAttributeNS(null, "cy",               cy);
  obj.setAttributeNS(null, "r",                DOT_RADIUS);
  obj.setAttributeNS(null, "stroke",           colour);
  obj.setAttributeNS(null, "stroke-width",     DOT_STROKE_WIDTH);
  obj.setAttributeNS(null, "fill-opacity",     1);
  obj.setAttributeNS(null, "stroke-opacity",   1);
  obj.setAttributeNS(null, "stroke-dasharray", 0);
  obj.setAttributeNS(null, "fill",             "#ffffff");
  obj.setAttributeNS(null, "pointer-events",   "none");
  paper.documentElement.appendChild(obj);

  dotHollow = obj;   // save so that we can delete it
}

function legend_off(evt, surgery) {
  if(!inMSR) {
    if(typeof console != 'undefined' && console != null)
       console.warn("Warning: legend_off re-entry\n");
    return;               // ignore this mouseout
  }
  inMSR = 0;

  dotHollow.parentNode.removeChild(dotHollow);
  paper.documentElement.appendChild(dotClone);
}

/**
 * Draw the popup for a given surgery. This has to be done
 * dynamically, because a static set of objects will appear below
 * anything else which is drawn dynamically, and there's no way to
 * change z-ordering. This also gives me an opportunity to strech the
 * box for long surgery names.
 *
 * @param startx  Translation of top-left of popup box to required
 *                start position
 * @param starty  Translation of top-left of popup box to required
 *                start position
 * @param upright 1 if speech bubble is upright, 0 for upside-down
 */
function draw_popup(surgery, startx, starty, upright) {
   var obj, txt1, txt2, txt3;
  var str;
  var width1, width2, width3, width4, extension;
  var textx, texty;

  // get the drawing position for the first line of text
  if(upright) {
    textx = startx + 9;
    texty = starty + 16;
  } else {
    textx = startx + 9;
    texty = starty + 28;
  }

  // create the text first, so that we can find out how wide it is. we use the
  // short-form text for the DHC surgeries, and code for the other surgeries
  var found = false;
  for(var i=0; i<ourSurgeries.length; ++i) {
     if(ourSurgeries[i][1] == surgery[0]) {
        found = true;
        str = paper.createTextNode(ourSurgeries[i][2]);
        break;
     }
  }
  if(!found)
     str = paper.createTextNode(surgery[5]);
  txt1 = paper.createElementNS(svgns, "text");

  txt1.setAttributeNS(null, "id",               "pbox4");
  txt1.setAttributeNS(null, "x",                textx);
  txt1.setAttributeNS(null, "y",                texty);
  txt1.setAttributeNS(null, "font-family",      font_popup);
  txt1.setAttributeNS(null, "font-size",        12);
  txt1.setAttributeNS(null, "font-weight",      "bold");
  txt1.setAttributeNS(null, "stroke",           "none");
  txt1.setAttributeNS(null, "stroke-width",     0);
  txt1.setAttributeNS(null, "fill-opacity",     1);
  txt1.setAttributeNS(null, "stroke-opacity",   1);
  txt1.setAttributeNS(null, "stroke-dasharray", 0);
  txt1.setAttributeNS(null, "fill", "           #000000");
  txt1.appendChild(str);
  paper.documentElement.appendChild(txt1);

  width1 = txt1.getComputedTextLength() + 18;   // left and right text margin

  // the second text line
  texty += 12;                                  // next line is 12px down
  str = paper.createTextNode(surgery[3]); // the Y value
  txt2 = txt1.cloneNode(false);
  txt2.setAttributeNS(null, "id",              "pbox5");
  txt2.setAttributeNS(null, "y",                texty);
  txt2.setAttributeNS(null, "font-size",        10);
  txt2.appendChild(str);
  paper.documentElement.appendChild(txt2);

  width2 = txt2.getComputedTextLength() + 18;

  // the third text line
  texty += 11;                                  // next line is 11px down
  str = paper.createTextNode(surgery[2] + " events");
  txt3 = txt1.cloneNode(false);
  txt3.setAttributeNS(null, "id",              "pbox6");
  txt3.setAttributeNS(null, "y",                texty);
  txt3.setAttributeNS(null, "font-size",        10);
  txt3.appendChild(str);
  paper.documentElement.appendChild(txt3);

  width3 = txt3.getComputedTextLength() + 18;

  /* find the required box size as 'width4'. we have a minimum box
   * size, to make sure that the pointer is drawn correctly.
   * 'extension' is the length of the final horizontal segment after
   * the pointer has returned to the box. note that the widths are
   * returned as zero before 'appendChild' */
  width4 = (width1 > width2)? width1 : width2;
  width4 = (width3 > width4)? width3 : width4;

  width4 = (width4 > 56)? width4 : 56;          // minimum size
  extension = width4 - 52;                      // extension >= 4 pixels

  var xpos, ypos;
  if(upright) {
    xpos = startx + 8;
    ypos = starty;
  } else {
    xpos = startx + 8;
    ypos = starty + 55;
  }

  obj = paper.createElementNS(svgns, "path");
  obj.setAttributeNS(null, "id", "pbox1");

  if(upright) {
    obj.setAttributeNS(
       null, "d",
       "M" + xpos + " " + ypos + " " +
       "a4 4 0 0 0 -4  4 " +
       "v35 "              +
       "a4 4 0 0 0  4  4 " +
       "h32 "              +
       "l-12 12 "          +
       "l24 -12 "          +
       "h" + extension     +
       "a4 4 0 0 0  4 -4 " +
       "v-35 "             +
       "a4 4 0 0 0 -4 -4 " +
       "Z");
  } else {
    obj.setAttributeNS(
       null, "d",
       "M" + xpos + " " + ypos + " " +
       "a4 4 0 0 1 -4 -4 " +
       "v-35 "             +
       "a4 4 0 0 1  4 -4 " +
       "h32 "              +
       "l-12 -12 "         +
       "l24 12 "           +
       "h" + extension     +
       "a4 4 0 0 1  4  4 " +
       "v35 "              +
       "a4 4 0 0 1 -4  4 " +
       "Z");
  }
  obj.setAttributeNS(null, "stroke",           "none");
  obj.setAttributeNS(null, "stroke-width",     0);
  obj.setAttributeNS(null, "fill-opacity",     0.3);
  obj.setAttributeNS(null, "stroke-opacity",   1);
  obj.setAttributeNS(null, "stroke-dasharray", 0);
  obj.setAttributeNS(null, "fill",             "#999999");
  obj.setAttributeNS(null, "transform",        "translate(2, 2)");
  paper.documentElement.appendChild(obj);

  var obj2 = obj.clone();
  obj2.setAttributeNS(null, "id", "pbox2");
  obj2.setAttributeNS(null, "fill-opacity",     0.6);
  obj2.setAttributeNS(null, "transform",        "translate(1, 1)");
  paper.documentElement.appendChild(obj2);

  var obj3 = obj.clone();
  obj3.setAttributeNS(null, "id", "pbox3"); 
  obj3.setAttributeNS(null, "stroke",           "#999999");
  obj3.setAttributeNS(null, "stroke-width",     1);
  obj3.setAttributeNS(null, "fill-opacity",     1);
  obj3.setAttributeNS(null, "fill",             "#ffffff");
  obj3.setAttributeNS(null, "transform",        "translate(0, 0)");
  paper.documentElement.appendChild(obj3);

  /* Ok, now for the stupid bit. we can't see the text, because the
   * box is in front.  so, re-append the text so that it goes in
   * front... */
  paper.documentElement.appendChild(txt1);
  paper.documentElement.appendChild(txt2);
  paper.documentElement.appendChild(txt3);
} // draw_popup()

function init_assert(cond, number) {
  if(!cond)
    throw new Error("internal error " + number + ": please report");
}

function draw_title_text() {
  var titleText, obj, x = GRID_LEFT * 4, y = GRID_ABOVE - 15, group = paper.set();

  // the text template
  titleText = paper.text(x, y, title).initZoom().setAttr({
    "font-family": font_title,
    "font-size": 16,
    "stroke": "none",
    "stroke-width": 0,
    "fill-opacity": 1,
    "stroke-opacity": 1,
    "stroke-dasharray": 0,
    "fill": "#222222"
  });


  y = GRID_ABOVE + (nYblocks*yBlockPx) + 46;
  obj = titleText.clone().initZoom().setAttr({ "y": y, "text": xaxis, "font-size": 12});
  group.push(obj);

  y += 4;
  for(var i=0; i<comment.length; i++) {
     y += 12;
     obj = titleText.clone().initZoom().setAttr({ "y": y, "text": comment[i],  "font-size": 10});
     group.push(obj);
  }

  x = GRID_LEFT - 40;
  y = (GRID_ABOVE + (nYblocks*yBlockPx) )/2;
  obj = titleText.clone().initZoom().setAttr({ "x": x, "y": y, "text": yaxis,  "font-size": 12}).rotate(-90);
  group.push(obj);
} // draw_title_text()

/**
 * Draw the background grid, and the labels on the axes. The whole lot
 * is created as a single 'g' element. This should have clipping in it
 * as well (I think), but I haven't done that yet.
 */
function draw_grid() {
   var group, obj;
   var grid_width    = nXblocks * xBlockPx;  // excludes last pixel
   var grid_height   = nYblocks * yBlockPx;  // excludes last pixel
   var canvas_width  = GRID_LEFT  + GRID_RIGHT + grid_width;
   var canvas_height = GRID_ABOVE + GRID_BELOW + grid_height;

   group = paper.set();

   var i, line, x = GRID_LEFT, y = GRID_ABOVE;
   // the basic line; this is cloned as required. we start with a
   // vertical line
   line = paper.rect(x, y, 1, grid_height + LineOverHangDown).initZoom().setAttr({
        "stroke": "none",
        "stroke-width": 0,
        "fill-opacity": 1,
        "stroke-opacity":   1,
        "stroke-dasharray": 0
    });

   // the vertical lines
   for(i=0; i <= nXblocks; ++i) {
     obj = line.clone().initZoom().setAttr({ "x": x, "fill": (i == 0 ? "#333333" : "#cccccc")
     });
     group.push(obj);
     x += xBlockPx;
   }

   // the horizontal lines
   x = GRID_LEFT - LineOverHangLeft;
   line.initZoom().setAttr({ "x": x, "width": grid_width + LineOverHangLeft, "height": 1});
   y = GRID_ABOVE;
   for(i=0; i <= nYblocks; ++i) {
     obj = line.clone().initZoom().setAttr({ "y": y, "fill": (i == nYblocks ? "#333333" : "#cccccc")});
     group.push(obj);
     y += yBlockPx;
   }

   /* draw faint vertical lines between the major lines, with no
    * overhang.  the faint lines are drawn in this order so taht the
    * left and right end-points appear on top of the two dark axes;
    * this gives a dot at the edges and looks better */
   x = GRID_LEFT + xBlockPx/2;
   line.initZoom().setAttr({ "y": GRID_ABOVE, "width":  1, "height": grid_height+1});
   for(i=0; i < nXblocks; ++i) {
     obj = line.clone().initZoom().setAttr({ "x": x, "fill": "#f0f0f0"});
     group.push(obj);
     x += xBlockPx;
   }

   // draw faint horizontal line between the major lines, with no overhang
   y = GRID_ABOVE + yBlockPx/2;
   line.initZoom().setAttr({ "x": GRID_LEFT, "width":  grid_width+1, "height": 1});
   for(i=0; i < nYblocks; ++i) {
     obj = line.clone().initZoom().setAttr({ "y": y, "fill": "#f0f0f0"});
     group.push(obj);
     y += yBlockPx;
   }

   /* the last step put faint dots (#f0f0f0) all the way around the
    * periphery at the axis cross points. this (selectable) step puts
    * white dots on the two axes, in case it looks better */
   if(DO_AXIS_DOTS) {
     var dot = paper.circle(x, y, 0.75).initZoom().setAttr({
        "stroke": "#DCF5F9",
        "stroke-width": 1,
        "fill-opacity": 1,
        "stroke-opacity": 1,
        "stroke-dasharray": 0,
        "fill": "#DCF5F9"
    });

     // main Y-axis dots
     y = GRID_ABOVE + 0.5;
     dot.initZoom().setAttr({ "x": GRID_LEFT + 0.5});
     for(i=0; i <= nYblocks; ++i) {
       obj = dot.clone().initZoom().setAttr({ "y": y});
       group.push(obj);
       y += yBlockPx;
     }

     // faint Y-axis dots
     y = GRID_ABOVE + yBlockPx/2 + 0.5;
     for(i=0; i < nYblocks; ++i) {
       obj = dot.clone().initZoom().setAttr({ "y": y});
       group.push(obj);
       y += yBlockPx;
     }

     // main X-axis dots
     x = GRID_LEFT + 0.5;
     dot.initZoom().setAttr({ "y": GRID_ABOVE + grid_height + 0.5});
     for(i=0; i <= nXblocks; ++i) {
       obj = dot.clone().initZoom().setAttr({ "x": x});
       group.push(obj);
       x += xBlockPx;
     }

     // faint X-axis dots
     x = GRID_LEFT  + xBlockPx/2 + 0.5;
     for(i=0; i < nXblocks; ++i) {
       obj = dot.clone().initZoom().setAttr({ "x": x});
       group.push(obj);
       x += xBlockPx;
     }
   } // if(DO_AXIS_DOTS)

   // the basic text string
   var tnode, str = "";
   tnode = paper.text(GRID_LEFT - Y_LGND_OFFS_LEFT, y, str).initZoom().setAttr({
        "text-anchor": "middle",
        "font-family": font_numbers,
        "font-size": 12,
        "text-anchor": "end",
        "stroke": "none",
        "stroke-width": 0,
        "fill-opacity": 1,
        "stroke-opacity": 1,
        "stroke-dasharray": 0,
        "fill": "#222222"
    });

   // draw the rates along the vertical axis
   y = GRID_ABOVE + Y_LGND_OFFS_DOWN;
   for(i=0; i <= nYblocks; ++i) {
      obj = tnode.clone().initZoom().setAttr({ "y": y, "text": stringsY[i]});
      group.push(obj);
      y += yBlockPx;
   }

   // draw the population sizes along the x axis. these need to be
   // rotated to fit
   y = GRID_ABOVE + grid_height + X_LGND_OFFS_DOWN * 1.5 ;
   tnode.initZoom().setAttr({ "y": y});
   x = GRID_LEFT + X_LGND_OFFS_RIGHT;
   for(i=0; i <= nXblocks; ++i) {
     obj = tnode.clone().initZoom().setAttr({ "x": x, "text": stringsX[i]}).rotate(-55);
     group.push(obj);
     x += xBlockPx;
   }
} // draw_grid()

/**
 * convert the main surgery index into an index into 'ourSurgeries' and
 * 'colours'. this should assert on failure, but I've currently only got a
 * throw back to the init call
 *
 * @return -1 if 'sindex' is not a DHC surgery, or the index into
 *         'ourSurgeries' otherwise
 */
function convertSurgeryIndexToDHCIndex(sindex) {
   for(var i=0; i<ourSurgeries.length; ++i) {
      if(ourSurgeries[i][0] == sindex)
         return i;
   }
   return -1;
}

/**
 * This path just touches the inside of the axes bounding box, and doesn't
 * overwrite the outside grid pixels.
 */
function set_chart_clip_path() {
   var defs, clipPath, rect;

   defs     = paper.createElementNS(svgns, "defs");
   clipPath = paper.createElementNS(svgns, "clipPath");
   rect     = paper.createElementNS(svgns, "rect");

   rect.setAttributeNS(null, "x",                GRID_LEFT+1);
   rect.setAttributeNS(null, "y",                GRID_ABOVE+1);
   rect.setAttributeNS(null, "width",            nXblocks*xBlockPx-1);
   rect.setAttributeNS(null, "height",           nYblocks*yBlockPx-1);
   rect.setAttributeNS(null, "stroke",           "none");
   rect.setAttributeNS(null, "stroke-width",     0);
   rect.setAttributeNS(null, "fill-opacity",     1);
   rect.setAttributeNS(null, "stroke-opacity",   1);
   rect.setAttributeNS(null, "stroke-dasharray", 0);
   rect.setAttributeNS(null, "fill",             "none");

   clipPath.setAttributeNS(null, "id", "chartClip");
   clipPath.appendChild(rect);
   defs.appendChild(clipPath);
   paper.documentElement.appendChild(defs);
}  // set_clipPath()

