/*
 * JavaScript code to support a summary data viewer for GAIA.
 *
 * Depends on: gaia_ws.js
 *             gaia_date.js
 *
 * Provides core features for the viewer. Code to support a specific
 * implmentation should not be included here.
 */

if (!window.gaiaViewer) 
  var gaiaViewer = new Object;

// If a sprintf function exists then import it, else use the one
// from gaiaSprintf
if (window.sprintf)
  gaiaViewer.sprintf = window.sprintf;
 else
   gaiaViewer.sprintf = gaiaSprintf.sprintf;


// Keep a reference to the viewer window inside the gaiaViewer object
// so that other windows can easily find the viewer window
if (!gaiaViewer.window)
  gaiaViewer.window = window;

if (!gaiaViewer.windowlist) {
  gaiaViewer.windowlist = new Object;
  gaiaViewer.windowlist.viewer = window;
 }

// Store all state information in one object. The order in which
// they are defined determines the order in which the callbacks are
// executed when multiple values are changed/

gaiaViewer.state = new Object;

// The selected time, in UT, as JavaScript Date object. The time
// portion is not required to be 00:00:00.
gaiaViewer.state.time = gaiaDate.getUTCYesterday();

// A string indicating which mode the window is operating in. Should
// be one of two values, "overview" or "detailedView".
gaiaViewer.state.mode = "overview";

// An array holding the a selected group of channels (eg for overview
// mode). Initialise to showing all channels. See also
// selectedChannel, which is just one value and must be a member of
// gaiaViewer.state.channels
gaiaViewer.state.channels = null; // populate later

gaiaViewer.state.selectedChannel = null; // populate later // gaiaViewer.state.channels[0];

// The palette used for displaying keograms and thumbnails.
// gaiaViewer.state.palette = gaiaWS.data.palettes[0];
gaiaViewer.state.palette = null; // populate later

// The orientation used when displaying keogram and thumbnails
// gaiaViewer.state.orientation = gaiaWS.data.orientations[0];
gaiaViewer.state.orientation = null; // populate later

// The channel order used when displaying data from multiple sites
gaiaViewer.state.channelOrder = null; // populate later

// Store a copy of the previous state. Have gaiaViewer.init()
// initialise it.
gaiaViewer.previousState = new Object;

// Sorted copy of channels, generated by a callback initiated by updateState
gaiaViewer.channelsSorted = new Array; // initialise later

// Object where field values are the channel DBIDs, for convenience
// when testing if a channel is selected (and opefully
// efficiency!). Follows gaiaViewer.state.channels and is updated by
// updateState
gaiaViewer.channelDbids = new Object;

// List of callbacks; when something important changes some other
// functions should be called to update their status.
gaiaViewer.callbacks = new Array;

// Register a callback to be run against one or more state
// changes. states can be a string or an array of strings. There is
// no unregister function but a reference to the window in which the
// register function was called from is kept. If the window is later
// closed the callback is removed when updateState() or
// doCallbacks() is next executed.
gaiaViewer.registerCallback = function (win, cbFunc, states, cbFuncName) {
  if (win.closed) {
    gaia.debuglog("Ignoring attempt to register callback with closed window")
  }
  if (states === null || states === undefined)
    states = new Array;

  var obj = {
    "callback": cbFunc,
    "states": states,
    "functionName": cbFuncName,
    "window": win // remember which window registered the callback
  };

  gaia.debuglog('gaiaViewer.registerCallback(), window', obj.window);

  if (cbFuncName)
    obj.functionName = cbFuncName;
  gaiaViewer.callbacks.push(obj);

  gaia.debuglog("gaiaViewer.registerCallback(): registered callback " 
		+ obj.functionName);
  return;
};


// Update the state, possibly changing multiple values at the same
// time. Call the necessary callbacks

gaiaViewer.updateState = function (obj) {
  var name;

  //var changes = new Array;
  delete obj.date; // not a valid state change, use time

  if (obj.channels && obj.channels.length == 0)
    delete obj.channels; // not valid to set no channels

  // The new value for gaiaViewer.channelDbids. Unless it needs to be
  // changed make it a reference to the existing object
  var newChannelDbids;
  var chanList = gaiaViewer.state.channels; // current list
  if (obj.channels) {
    obj.channels = gaia.unique(obj.channels, 
			       function (a, b) { 
				 return a.getDbid() - b.getDbid() });
    
    // recreate the channelDbids object
    // newChannelDbids = gaiaWS.getDbids(obj.channels);
    newChannelDbids = new Object;
    for (var i = 0; i < obj.channels.length; ++i)
      newChannelDbids[obj.channels[i].getDbid()] = true;

    chanList = obj.channels; // list will be updated
  }
  else 
    newChannelDbids = gaiaViewer.channelDbids;


  if (obj.selectedChannel) {
    // The selected channel is being changed, is it in the channel
    // list? If not add it.
    
    if (!gaia.arrayIntersect(gaiaWS.getDbids(chanList), 
			     [ obj.selectedChannel.getDbid() ])) {
      // The selected channel isn't in the list, add it. Take care
      // because chanList could be a reference to
      // gaiaViewer.state.channels. concat will give a new array
      obj.channels = chanList.concat(obj.selectedChannel);
      newChannelDbids[obj.selectedChannel.getDbid()] = true;
    }
    
  }
  else 
    if (obj.channels) {
      // Check if the new channel list includes the currently-selected
      // channel. If not change choose the first from the list.
      if (!gaia.arrayIntersect(gaiaWS.getDbids(chanList),
			       [ gaiaViewer.state.selectedChannel.getDbid() ]))
	obj.selectedChannel = obj.channels[0];
    }
  
  // See which of the values in obj actually changed
  var haveChanges = false;
  if (obj == gaiaViewer.state)
    // don't test for equality and don't remove elements from the
    // viewer state object!
    haveChanges = true;
  else
    for (name in obj) {
      if (name == "time" 
	  && gaiaDate.areEqual(obj.time, gaiaViewer.state.time))
	delete obj[name];
      else {
	if (obj[name] == gaiaViewer.state[name])
	  delete obj[name];
	else
	  haveChanges = true;
      }
    }
  

  if (haveChanges == false)
    return; // nothing changed

  // Allow functions to be notified when the date (not just time
  // of day) has changed
  if (obj.time  && 
      !gaiaDate.areEqual(gaiaDate.getUTCStartOfDay(obj.time),
			 gaiaDate.getUTCStartOfDay(gaiaViewer.state.time))) {
    obj.date = obj.time;
  }


  // Update the state variables
  for (name in obj) {
    // Save current state and then update. Ignore "date" as it is
    // derived from time
    if (name != "date") {
      gaiaViewer.previousState[name] = gaia.clone(gaiaViewer.state[name]);
      gaiaViewer.state[name] = obj[name];
    }
  }

  // update the object holding the DBIDs for the channels
  //gaiaViewer.channelDbids = new Object;
  //for (var i = 0; i < gaiaViewer.state.channels.length; ++i)
  //gaiaViewer.channelDbids[gaiaViewer.state.channels[i].getDbid()] = true;
  gaiaViewer.channelDbids = newChannelDbids;
  
  //update the sorted array
  if (!gaiaViewer.state.channelOrder)
    gaiaViewer.state.channelOrder = gaiaWS.data.channelOrders[0];
  gaiaViewer.channelsSorted 
  = gaiaWS.sortChannels(gaiaViewer.state.channels, 
			gaiaViewer.state.channelOrder);
  
  if (!gaiaViewer.state.orientation)
    gaiaViewer.state.orientation = gaiaWS.data.orientations[0];

  gaiaViewer.doCallbacks(obj);
  return;
};

gaiaViewer.doCallbacks = function (stateChange) {
  // State variables which changed
  window.document.body.style.cursor = "wait";
  var changes = new Array;
  for (var name in stateChange)
    changes.push(name);

  var i = 0;
  while (i < gaiaViewer.callbacks.length) {
    var obj = gaiaViewer.callbacks[i];
    if (obj.window.closed) {
      // remove old callback
      gaia.debuglog("gaiaViewer.doCallbacks(): removing " + obj.functionName);

      gaiaViewer.callbacks.splice(i, 1);
    }
    else {
      // Check if the callback should be executed
      if (obj.states.length == 0 
	  || gaia.arrayIntersect(obj.states, changes)) {

	// Execute the callback
	gaia.debuglog("gaiaViewer.doCallbacks(): calling " + obj.functionName,
		      stateChange);

	// console.log("doing callback: " + obj.functionName);
	obj.callback(stateChange);
      }
      ++i; // examine next callback object
    }
      
  }
  window.document.body.style.cursor = "";
  return;
};


// The data availability object for the selected day. This isn't
// treated like a state variable because it should be considered as
// read-only by everyone except gaiaViewer.dataAvailabilityCB. If
// tools need to be updated when it changes they should register a
// callback for "time" state changes.
gaiaViewer.dataAvailability = null;

// Keep references to the various HTMLElements used for the
// overview. Then it will be easy to make individual ones hidden or
// to alter their order.
gaiaViewer.overviewModeData = {
  "currentChannels": null,
  "channelBlock": { },
  "imgBlock": { },
  "img": { },
  numVisibleChannels: null
};
 

// Static data for the detailed view mode functions
gaiaViewer.detailedViewModeData = {
  "channel": null, // the channel of the currently set keogram
  "time": null, // 
  "thumbnailSize": 96,    //change to 128 (bigger)
  "border": 0,
  "imgOffsets": [-2, -1, 0, 1, 2], // in cadences (ie normally 1 minute)
  "img": { },
  "imgTimestamps": { },
  "imgContainers": { },
  "keogram": null,
  "keogramSrc": null, // the img src attribute will vary due to mirroring
  "keogramHeight": 128,  //change to 128 (bigger)
  "keogramWidth": 720,
  "timeLine": null,  
  "timeLineWidth": 2,
  "timeLineHeight": 128  //change to 128 (bigger)
};

gaiaViewer.dataAvailabilityCB = function () {
  gaiaViewer.dataAvailability
  = new gaiaWS.DataAvailability(gaiaViewer.state.time);
};
  

// Open the toolbox window. This function should only be called from
// the summary plot window, so the caller must be the summary plot
// window. Store a reference to the toolbox window in summary plot
// window.
gaiaViewer.toolboxWindow = null;
gaiaViewer.openToolbox = function () {
  if (!gaiaViewer.toolboxWindow ||
      gaiaViewer.toolboxWindow.closed) {
    // open a new window
    gaiaViewer.toolboxWindow 
    = window.open("toolbox", "gaia_toolbox", 
		  "width=280,height=300,status=yes,resizable=yes");
    if (!gaiaViewer.toolboxWindow)
      // popups must be disabled
      window.location = gaiaWS.config.enable_popups_url;

  }
  else {
    // already open, try to switch focus
    gaiaViewer.toolboxWindow.focus();
  }
  // import data from toolbox window
  // gaiaViewer.gaiaToolbox = gaiaViewer.toolboxWindow.gaiaToolbox;
  return true;
};




function gaiaGetThumbnailUrl(project, station, channel,
			     palette, orientation, timestamp) {
  var orNs = orientation.substr(0, 1);
  var orUd = orientation.substr(1, 1);

  if (orNs == 'e')
    if (station.getLatitude() >= 0)
      orNs = 's';
    else
      orNs = 'n';

  if (orNs == 'p')
    if (station.getLatitude() >= 0)
      orNs = 'n';
    else
      orNs = 's';
  
  var r;
  if (!channel.getChannelType().getIsGreyscale())
    palette = 'none';
  
  r = '/palette/' + palette + '/thumbnail/' 
    + orNs + orUd + '/'
    + project.getAbbreviation().toLowerCase( ) + '/'
    + station.getAbbreviation().toLowerCase( ) + '/'
    + channel.channel_type.getRefName().toLowerCase( ) 
    + '/%Y-%m-%dT%H:%M:%SZ';
  
  if (timestamp != null)
    r = strftime(timestamp, r, true);
  return r;
}


gaiaViewer.chooseProjectChanged = false;
gaiaViewer.chooseProject = function (elem) {
  var channels;
  var projDbid = elem.options[elem.selectedIndex].value;
  switch (projDbid) {
  case "-2":
    // "custom selection", leave as it is
    return;
    break;

  case "-1":
    // "All projects"
    // channels = gaiaWS.jsonData.channels.channel;
    channels = gaiaWS.data.channels;
    break;
    
  default:
    // Add channels corresponding to selected project
    channels = gaiaWS.getChannelsByProjectDbid(projDbid);
    break;
  }
    
  // mark that the viewer menu option initiated the callback
  gaiaViewer.chooseProjectChanged = true;
  gaiaViewer.updateSelectChannels({"channels": channels});
 
};



gaiaViewer.chooseProjectCB = function () {
  if (gaiaViewer.chooseProjectChanged != true) {
    // someone else made a change to the list of channels, set to the
    // custom entry
    var e = document.getElementById('choose_project');
    if (e)
      e.selectedIndex = 0;
  }

  gaiaViewer.chooseProjectChanged = false;
};



gaiaViewer.modeButtonOnClick = function (mode) {
  // gaiaViewer.state.mode = mode;
  // gaiaViewer.doCallbacks("mode");
  gaiaViewer.updateState({"mode": mode});
};


gaiaViewer.gaiaModeCB = function () {
  var odiv = gaiaViewer.windowlist.viewer.document.getElementById("overview-div");
  var ddiv = gaiaViewer.windowlist.viewer.document.getElementById("detailed-view-div");
  
  if (!odiv) {
    console.warn("Cannot find overview div");
    return false;
  }
  if (!ddiv) {
    console.warn("Cannot find detailed view div");
    return false;
  }

  switch (gaiaViewer.state.mode) {
    case "overview":
    odiv.style.display = "block";
    ddiv.style.display = "none";
    break;

    case "detailedView":
    odiv.style.display = "none";
    ddiv.style.display = "block";
    break;
      
    default:
    console.warn("Unknown mode: " + gaiaViewer.state.mode);
    break;
  }

  return;
};
  

// Return a list of buttons (some with images on them). Let caller
// assign a callback function. Value attribute contains the palette
// DBID .
gaiaViewer.getPaletteButtons = function (doc) {
  if (!doc)
    doc = window.document;

  var r = new Array;
  for (var i = 0; i < gaiaWS.data.palettes.length; ++i) {
    var p = gaiaWS.data.palettes[i];
    var url = p.getUrl();
    var elem = doc.createElement('button');
    elem.value = p.getDbid();
    elem.title = p.getDescription();
    if (url) {
      var img = doc.createElement('img');
      img.alt = p.getName();
      img.src = url;
      img.width = 64;
      img.height = 12;
      img.border = 1;
      img.title = p.getDescription();
      elem.appendChild(img);
      // insert a non-breaking space so that the button height is the
      // same for all

      elem.appendChild(doc.createTextNode("\u00a0"));
    }
    else {
      elem.appendChild(doc.createTextNode(p.getName()));
    }
    r.push(elem);
  }
  return r;
};



// Return a select element which allows user to select a project. User
// should set the callbacks as appropriate
gaiaViewer.projectControl = function (doc) {
  if (!doc)
    doc = document;

  var sel = doc.createElement('select');
  sel.title = "Select project";
  
  // Add two additional options before the list of projects. Don't
  // allow the user to select custom selection from the menu, unless
  // the selected channels is already a custom list. Note that
  // gaiaViewer.projectControlOnChange assumes -2 and -1 have the same
  // special meaning.
  var opt;
  opt = doc.createElement('option');
  opt.value = -2;
  opt.disabled = true;
  opt.appendChild(doc.createTextNode("Custom selection"));
  sel.appendChild(opt);
  opt = doc.createElement('option');
  opt.value = -1;
  opt.appendChild(doc.createTextNode("All projects"));
  sel.appendChild(opt);

  for (var i = 0; i < gaiaWS.data.projects.length; ++i) {
    var p = gaiaWS.data.projects[i];
    opt = doc.createElement('option');
    opt.value = p.getDbid();
    opt.appendChild(doc.createTextNode(p.getAbbreviation()));
    sel.appendChild(opt);
  }

  return sel;
};


gaiaViewer.projectControlOnChange = function (elem) {
  var projDbid = parseInt(elem.options[elem.selectedIndex].value);
  switch (projDbid) {
  case -2:
    // custom selection, leave as is
    break;
    
  case -1:
    // all projects, so all channels
    gaiaViewer.updateState({"channels": gaiaWS.data.channels});
    break;
    
  default:
    gaiaViewer.updateState({"channels": 
			     gaiaWS.getChannelsByProjectDbid(projDbid)});
  }
  return true;
};

  

gaiaViewer.projectControlRefresh = function(elem, channels) {
  var dbid = gaiaWS.getProjectDbidFromChannels(channels);
  var found = false;

  // find corresponding value in menu. At same time look for the
  // custom option and make it disabled.
  for (var i = 0; i < elem.options.length; ++i) {
    if (elem.options[i].value == -2)
      // Found the "Custom selection" option, set it to be disabled
      elem.options[i].disabled = true; 
    
    if (elem.options[i].value == dbid) {
      // Found the option required, ensure it is not disabled (could
      // be "Custom selection"). Then make it the chosen option
      elem.options[i].disabled = false;
      elem.selectedIndex = i;
      found = true;
    }
  }
  
  if (!found)
    console.warn('gaiaViewer.projectControlRefresh(): Could not find option ' 
		 + 'for DBID ' + dbid);
  return;
};

gaiaViewer.orientationControl = function (doc) {
  if (!doc)
    doc = document;
  var sel = doc.createElement('select');
  sel.title = "Select keogram/thumbnail orientation";

  for (var i = 0; i < gaiaWS.data.orientations.length; ++i) {
    var p = gaiaWS.data.orientations[i];
    var opt = doc.createElement('option');
    opt.value = p.getDbid();
    opt.appendChild(doc.createTextNode(p.getDescription()));
    sel.appendChild(opt);
  }

  return sel;
};


gaiaViewer.orientationControlOnChange = function (elem) {
  var dbid = parseInt(elem.options[elem.selectedIndex].value);
  gaiaViewer.updateState({"orientation": 
			   gaiaWS.getOrientationByDbid(dbid)});
  return true;
};


gaiaViewer.orientationControlRefresh = function(elem, orientation) {
  var dbid = orientation.getDbid();

  // find corresponding value in menu
  for (var i = 0; i < elem.options.length; ++i) {
    if (elem.options[i].value == dbid) {
      elem.selectedIndex = i;
      return true;
    }
  }
  
  console.warn('gaiaViewer.orientationControlRefresh(): Could not '
	       + 'find option for DBID ' + dbid);
  return false;
};


gaiaViewer.channelOrderControl = function (doc) {
  if (!doc)
    doc = document;

  var sel = doc.createElement('select');
  sel.title = "Select keogram/thumbnail channel order";

  for (var i = 0; i < gaiaWS.data.channelOrders.length; ++i) {
    var p = gaiaWS.data.channelOrders[i];
    var opt = doc.createElement('option');
    opt.value = p.getDbid();
    opt.appendChild(doc.createTextNode(p.getDescription()));
    sel.appendChild(opt);
  }

  return sel;
};


gaiaViewer.channelOrderControlOnChange = function (elem) {
  var dbid = parseInt(elem.options[elem.selectedIndex].value);
  gaiaViewer.updateState({"channelOrder": 
			   gaiaWS.getChannelOrderByDbid(dbid)});
  return true;
};


gaiaViewer.channelOrderControlRefresh = function(elem, channelOrder) {
  var dbid = channelOrder.getDbid();

  // find corresponding value in menu
  for (var i = 0; i < elem.options.length; ++i) {
    if (elem.options[i].value == dbid) {
      elem.selectedIndex = i;
      return true;
    }
  }
  
  console.warn('gaiaViewer.channelOrderControlRefresh(): Could not '
	       + 'find option for DBID ' + dbid);
  return false;
};



// Remove all decendents (ie children,, grand children etc)
gaiaViewer.removeAllDescendents = function (node) {
  while (node.hasChildNodes()) {
    gaiaViewer.removeAllDescendents(node.firstChild);
    node.removeChild(node.firstChild);
  }
  return;
};


gaiaViewer.createOverviewStructure = function ( ) {
  var doc = gaiaViewer.windowlist.viewer.document;
  var elem = gaiaViewer.overviewModeData.container;
  if (!elem)
    elem = gaiaViewer.overviewModeData.container 
      = doc.getElementById('overview-content');
  
  if (!elem) {
    console.warn('Could not find div "overview-content"');
    return false;
  }
    
  // empty the container
  while (elem.hasChildNodes())
    elem.removeChild(elem.firstChild);
  
  for (var i = 0; i < gaiaWS.data.channels.length; ++i) {
    // create new elements
    var chan = gaiaWS.data.channels[i];
    var chanDbid = chan.getDbid();
    var chanLabel = chan.getLabel();
      
    var chanBlock = doc.createElement("div");
    var imgBlock = doc.createElement("div");
    var labelBlock = doc.createElement("div");
    
    var proj = chan.getProject();
    var stat = chan.getStation();
    var ct = chan.getChannelType();
    /*
      labelBlock.appendChild(doc.createTextNode(proj.getAbbreviation()));
      labelBlock.appendChild(doc.createElement('br'));
      labelBlock.appendChild(doc.createTextNode(stat.getAbbreviation()));
      labelBlock.appendChild(doc.createElement('br'));
      labelBlock.appendChild(doc.createTextNode(ct.getName()));
    */
    /*
      labelBlock.appendChild(doc.createTextNode(proj.getAbbreviation() + ' ' +
      stat.getAbbreviation() + ' ' +
      ct.getName().replace(' ', 
      '&nbsp;')));
    */
    labelBlock.appendChild(doc.createTextNode(proj.getAbbreviation() + ' '));
    //labelBlock.appendChild(doc.createElement('br'));
    labelBlock.appendChild(doc.createTextNode(stat.getAbbreviation() + ' '));
    //labelBlock.appendChild(doc.createElement('br'));
    labelBlock.appendChild(doc.createTextNode(ct.getName()));



    chanBlock.className = "channel-block";
    chanBlock.id = "channel-block-" + chanDbid;
    imgBlock.className = "keogram-block";
    labelBlock.className = "keogram-label";
    chanBlock.appendChild(imgBlock);
    chanBlock.appendChild(labelBlock);

    // Do not set the src attribute! It interferes with gaiaMirror
    // (the initial image can fire the onload event and confuse
    // gaiaMirror).
    var img = doc.createElement('img');
    img.width = 720;
    img.height = 64;
    img.title = chanLabel;
    img.border = 1;
    img.id = "keogram-" + chanDbid;
    img.style.visibility = "hidden";
    imgBlock.appendChild(img);
    
    gaiaViewer.overviewModeData.channelBlock[chanDbid] = chanBlock;
    gaiaViewer.overviewModeData.imgBlock[chanDbid] = imgBlock;
    gaiaViewer.overviewModeData.img[chanDbid] = img;
  }

  // Now insert the new elements which correspond to the selected data
  // channels into the document.
  for (var i = 0; i < gaiaViewer.channelsSorted.length; ++i) {
    chanDbid = gaiaViewer.channelsSorted[i].getDbid();
    elem.appendChild(gaiaViewer.overviewModeData.channelBlock[chanDbid]);
  }

  // Keep a copy of the order which channels are shown and the order
  // used. When the channels are sorted a new array is created so we
  // don't need to clone it here, and not doing so allows us to
  // quickly compare if the order has changed.
  gaiaViewer.overviewModeData.currentChannels = gaiaViewer.channelsSorted;

  return true;
};


// must register to be called on the following changes: date (not
// time), channels, palette?, orientation?, channelOrder

gaiaViewer.overviewModeIgnoredChanges = new Object;
gaiaViewer.refreshOverviewContentCB = function (changes) {
  if (gaiaViewer.state.mode != "overview") {
    // Don't bother updating if running in another mode. However, if
    // changes other than switching the mode occured remember that
    // the update must occur later when the mode is switched to
    // overview
    for (var c in changes)
      gaiaViewer.overviewModeIgnoredChanges[c] = true;
    
    return false;
  }
  
  var chanDbid;

  if (!gaiaViewer.overviewModeData.container)
    gaiaViewer.createOverviewStructure();

  var elem = gaiaViewer.overviewModeData.container;

  // Is the current layout correct for the channels
  if (gaiaViewer.overviewModeData.currentChannels 
      != gaiaViewer.channelsSorted) {

    // Empty the container
    while (elem.hasChildNodes())
      elem.removeChild(elem.firstChild);
    
    // Fill it in the correct order
    for (var i = 0; i < gaiaViewer.channelsSorted.length; ++i) {
      chanDbid = gaiaViewer.channelsSorted[i].getDbid();
      elem.appendChild(gaiaViewer.overviewModeData.channelBlock[chanDbid]);
    }
    
    // Remember how the parts are laid out
    gaiaViewer.overviewModeData.currentChannels = gaiaViewer.channelsSorted;
  }
  
  // Are new images needed?
  if (changes.date || gaiaViewer.overviewModeIgnoredChanges.date
      || changes.channels || gaiaViewer.overviewModeIgnoredChanges.channels
      || changes.palette || gaiaViewer.overviewModeIgnoredChanges.palette
      || changes.orientation
      || gaiaViewer.overviewModeIgnoredChanges.orientation) {
    
    var border = 1;
    var height;

    // Have the keogram height dependent on how many keograms will be
    // displayed, that is a function of the data availability as well
    // as the channels selected.
    var count = 0;
    for (var i = 0; i < gaiaViewer.channelsSorted.length; ++i) {
      var chan = gaiaViewer.channelsSorted[i];
      if (gaiaViewer.dataAvailability.hasKeograms(chan) ||
	  gaiaViewer.dataAvailability.hasSummaryData(chan))
	++count;
    }
    
    if (count < 8)
      height = 64;
    else {
      if (count < 16)
	height = 32;
      else
	// this is a silly amount of keograms to display
	height = 16;
    }
    
    var dateLabel = gaiaViewer.state.time.UTCstrftime('%Y-%m-%d');

    // Need a running count of how many channels are visible. Have a
    // slight problem in that some images may have finished loading
    // (and called their onError handler) before having completed the
    // initialisation of all gaiaMirror objects. Therefore set the
    // number of visible channels to the maximum number and decrement
    // each time a channel cannot be displayed. Then even if the
    // onError handler is called that decrements the running
    // total. Should be free of race conditions as long as
    // pre-decrement is atomic.
    gaiaViewer.overviewModeData.numVisibleChannels = gaiaWS.data.channels.length;
    
    // show/hide the channels
    gaiaViewer.overviewModeNoDataWarning(gaiaViewer.overviewModeData.numVisibleChannels <= 0);

    for (var i = 0; i < gaiaWS.data.channels.length; ++i) {
      var chan = gaiaWS.data.channels[i];
      var chanDbid = chan.getDbid();
      
      var img;
      if (gaiaViewer.channelDbids[chanDbid]
	  && (gaiaViewer.dataAvailability.hasKeograms(chan)
	      || gaiaViewer.dataAvailability.hasSummaryData(chan))) {
	// Channel is selected and has keograms/summary data plots
	
	if (false) {
	  // Reuse existing images. This has concurrency issues with
	  // mirroring when the user is browsing quickly
	  img = gaiaViewer.overviewModeData.img[chanDbid];
	  
	  // If the user is browsing quickly the images may not have
	  // loaded, and there could still be timeouts in
	  // operation. Have gaiaMirror clean-up before setting
	  // initiating new mirroring operations
	  gaiaMirror.finished(img);
	}
	else {
	  // Replace the image with a new one. Safe for people who
	  // press the next day button before all the images have loaded.
	  img = gaiaViewer.windowlist.viewer.document.createElement('img');
	  img.width = 720;
	  img.height = height;
	  img.title = chan.getLabel();
	  img.id = "keogram-" + chanDbid;
	  gaiaViewer.overviewModeData.img[chanDbid].parentNode.replaceChild(img, gaiaViewer.overviewModeData.img[chanDbid]);
	  gaiaViewer.overviewModeData.img[chanDbid] = img;
	}
	
	img.style.visibility = "hidden";
	img.title = chan.getLabel() + ' ' + dateLabel;
	// img.height = height;
	img.border = border;
	
	// Want a closure for onclick but if it is done in this loop
	// all function get the same value of chan (reference). Call a
	// separate function to to create the closure so that all
	// calbacks have unique chan values
	img.onclick = gaiaViewer.makeCallbackForOverviewKeogram(chan);

	var src;
	var nodes;
	if (gaiaViewer.dataAvailability.hasKeograms(chan)) {
	  src = chan.getKeogramUrl(gaiaViewer.state.orientation,
				   gaiaViewer.state.time);
	  nodes = chan.getKeogramDataLocations().getNodes();
	}
	else {
	  // use summary data instead
	  src = chan.getSummaryDataUrl(gaiaViewer.state.time);
	  nodes = chan.getSummaryDataLocations().getNodes();
	}

	gaiaViewer.overviewModeData.imgBlock[chanDbid].style.minHeight
	  = height + 1 * border + "px";
	//gaiaViewer.overviewModeData.imgBlock[chanDbid].style.margin
	// = "1px 1px 1px 1px";
	
	// Ensure the channel block is a visible block
	gaiaViewer.overviewModeData.channelBlock[chanDbid].style.display
	  = "block";


	// INITIATE MIRRORING PROCESS
	var mir = new gaiaMirror.gaiaMirror(src, {"nodes": nodes});

	
	if (!gaiaViewer.dataAvailability.hasAvailability(chan)) {
	  // Don't have any availability information so create a
	  // callback function just in case the keogram fails to
	  // load. Then the entire channel block is made invisible. We
	  // don't use this for times when we know there should be
	  // data since we want to show the data could not be loaded.
	  mir.setAttribute("channelDbid", chanDbid);
	  mir.setOnError( function(t){ gaiaViewer.overviewKeogramOnError(t) });
	}

	mir.setMakeVisible(2);
	mir.init(img);
      }
      else {
	// Collapse the channel block and inidcate one less possible
	// channel to display
	gaiaViewer.overviewModeData.channelBlock[chanDbid].style.display
	  = "none";
       
	--gaiaViewer.overviewModeData.numVisibleChannels;
      }

    }
    
    // Now that the refresh has been performed clear the list of
    // ignored changes
    gaiaViewer.overviewModeIgnoredChanges = new Object;

    gaiaViewer.overviewModeNoDataWarning(!gaiaViewer.overviewModeData.numVisibleChannels);
  }

  
  return true;
};


gaiaViewer.makeCallbackForOverviewKeogram = function(chan) {
  return function (ev) { return gaiaViewer.keogramOnClick(ev, this, chan); };
};


gaiaViewer.keogramOnClick = function (ev, img, chan) {
  if (window.event)
    ev = window.event;
  
  // Get position where the mouse was clicked
  var mouseX = ev.clientX;
  // var mouseY = ev.clientY;
  
  // Get position of top-left of image element, on the outside of any
  // border
  var imgXY = gaia.getAbsoluteElementOffset(img);
  var border = parseInt(img.border);
  if (!isFinite(border))
    border = 0; // not set?

  // deduce approximate date/time
  var xPos = (mouseX - imgXY[0] - border);
  var tStartOfDay = gaiaDate.getUTCStartOfDay(gaiaViewer.state.time).valueOf();
  var t = tStartOfDay + (xPos/img.width)*86400e3;
  if (t < tStartOfDay)
    t = tStartOfDay; // limit to start of day
  else
    if (t >= (tStartOfDay + 86340e3))
      t = tStartOfDay + 86340e3; // limit to 23:59, have 1 min res data

  gaiaViewer.updateState({"time": new Date(t),
			     "selectedChannel": chan,
			     "mode": "detailedView"});
  return true;
};


// Handler for cases when the overview keogram fails to load. gm isa
// gaiaMirror object.
gaiaViewer.overviewKeogramOnError = function (gm) {
  var doc = gaiaViewer.windowlist.viewer.document;
  var chanDbid = gm.getAttribute('channelDbid');
  var id = "channel-block-" + chanDbid;
  var elem = doc.getElementById(id);
  
  // reduce the number of channels that are displayed
  --gaiaViewer.overviewModeData.numVisibleChannels;
  
  if (!elem) {
    console.log('Could find find overview channel block ' + id);
    return false;
  }
  elem.style.display = 'none';

  if (gaiaViewer.overviewModeData.numVisibleChannels <= 0)
    gaiaViewer.overviewModeNoDataWarning(true);
};


// If true show the no data warning and hide the keograms, else show
// the keograms and hide the warning
gaiaViewer.overviewModeNoDataWarning = function (a) {
  var doc = gaiaViewer.windowlist.viewer.document;
  var content = doc.getElementById('overview-content');
  var timeAxis = doc.getElementById('overview-time-axis');
  var noData = doc.getElementById('overview-no-data');

  if (a) {
    content.style.display = timeAxis.style.display = "none";
    noData.style.display = "block";
  }
  else {
    content.style.display = timeAxis.style.display = "block";
    noData.style.display = "none";
  }
};


gaiaViewer.createDetailedStructure = function ( ) {
  var doc = gaiaViewer.windowlist.viewer.document;
  var elem;
  var img;
  
  gaiaViewer.detailedViewModeData.img = new Object;
  gaiaViewer.detailedViewModeData.imgTimestamps = new Object;
  gaiaViewer.detailedViewModeData.imgContainers = new Object;

  for (var i = 0; i < gaiaViewer.detailedViewModeData.imgOffsets.length; ++i) {
    var offset = gaiaViewer.detailedViewModeData.imgOffsets[i];
    elem = doc.getElementById(gaiaSprintf.sprintf('detailed-view-thumbnail%d', offset));
    if (elem) {
      gaiaViewer.removeAllDescendents(elem);
      
      // set min height and width
      elem.style.minHeight = elem.style.minWidth = 
      (gaiaViewer.detailedViewModeData.thumbnailSize
       + 2 * gaiaViewer.detailedViewModeData.border) + 'px';
      
      // Create a dummy image. It will be replaced when the detailed
      // view is updated
      img = doc.createElement('img');
      img.width = img.height = gaiaViewer.detailedViewModeData.thumbnailSize;
      img.border = gaiaViewer.detailedViewModeData.border;
      img.src = gaiaWS.config.blank_image;
      elem.appendChild(img);
      gaiaViewer.detailedViewModeData.img[offset] = img;

      // Create timestamps for the images
      elem.appendChild(doc.createElement('br'));
      
      var timestamp = doc.createTextNode("HH:MM");
      gaiaViewer.detailedViewModeData.imgTimestamps[offset] = timestamp;
      elem.appendChild(timestamp);
      gaiaViewer.detailedViewModeData.imgContainers[offset] = elem;
    }
  }

  var idStr;
  idStr = "detailed-view-keogram";
  elem = doc.getElementById(idStr);
  if (elem && elem.tagName == "IMG") {
    elem.style.position = "absolute";
    elem.style.left = "0px";
    elem.width = gaiaViewer.detailedViewModeData.keogramWidth;
    elem.height = gaiaViewer.detailedViewModeData.keogramHeight;
    // img.style.cursor = "crosshair";
    gaiaViewer.detailedViewModeData.keogram = elem; 
  }
  else
    console.warn("Could not find a IMG with ID " + idStr);

  idStr = "detailed-view-keogram-timeline";
  elem = doc.getElementById(idStr);
  if (elem && elem.tagName == "IMG") {
    elem.style.position = "absolute";
    elem.style.left = "0px";
    elem.width = gaiaViewer.detailedViewModeData.timeLineWidth;
    elem.height = gaiaViewer.detailedViewModeData.timeLineHeight;
    gaiaViewer.detailedViewModeData.timeLine = elem; 
  }
  else
    console.warn("Could not find a IMG with ID " + idStr);
 
};

gaiaViewer.refreshDetailedViewContentCB = function (changes) {
  if (gaiaViewer.state.mode != "detailedView")
    // wrong mode, don't update
    return true;

  // // get time to nearest minute
  var cadence = gaiaViewer.state.selectedChannel.cadence; // in ms
  var t = gaiaDate.round(gaiaViewer.state.time, cadence);
  var src;
  var img;
  var oldImg;
  var chan = gaiaViewer.state.selectedChannel;
  var hasThumbnails = chan.hasThumbnails();
  for (var i = 0; i < gaiaViewer.detailedViewModeData.imgOffsets.length; ++i) {
    var offset = gaiaViewer.detailedViewModeData.imgOffsets[i];
    if (hasThumbnails) {
      oldImg = gaiaViewer.detailedViewModeData.img[offset];
      if (oldImg) {
	var tImg = new Date(t.valueOf() + offset * cadence);
      
	// create a new image
	var img = gaiaViewer.windowlist.viewer.document.createElement('img');
	img.width = img.height = gaiaViewer.detailedViewModeData.thumbnailSize;
	img.border = gaiaViewer.detailedViewModeData.border;
	img.style.visibility = "hidden";
      
	// need a callback function, but one which uses the current
	// value of tImg, not a reference to it (which would mean all
	// images set the same time)
	img.onclick = gaiaViewer.makeCallbackForDetailedViewThumbnail(tImg);
      
	// replace
	oldImg.parentNode.replaceChild(img, oldImg);
      
	gaiaViewer.detailedViewModeData.img[offset] = img;
      
	// update the timestamp
	gaiaViewer.detailedViewModeData.imgTimestamps[offset].data =
	  tImg.UTCstrftime('%H:%M UT');
      
	// Initiate mirroring
	src = chan.getThumbnailUrl(gaiaViewer.state.orientation,
				   tImg);
	var n = chan.getThumbnailDataLocations().getNodes();
	var mir = new gaiaMirror.gaiaMirror(src, {"nodes": n});  
	mir.setMakeVisible(2);
	mir.init(img);
      
      }
      gaiaViewer.detailedViewModeData.img[offset].style.visibility 
	= "visible";
      gaiaViewer.detailedViewModeData.imgContainers[offset].style.visibility 
	= "visible";
    }
    else {
      gaiaViewer.detailedViewModeData.img[offset].style.visibility 
	= "hidden";
      gaiaViewer.detailedViewModeData.imgContainers[offset].style.visibility 
	= "hidden";

    }
  }
        
  var nodes;
  // does the keogram need updating?
  if (gaiaViewer.dataAvailability.hasKeograms(chan)) {
    src = chan.getKeogramUrl(gaiaViewer.state.orientation,
			     gaiaViewer.state.time);
    nodes = chan.getKeogramDataLocations().getNodes();
  }
  else {
    // use summary data instead
    src = chan.getSummaryDataUrl(gaiaViewer.state.time);
    nodes = [];
    if (chan.getSummaryDataLocations().length)
      nodes = chan.getSummaryDataLocations().getNodes();
  }
  if (src != gaiaViewer.detailedViewModeData.src) {
    oldImg = gaiaViewer.detailedViewModeData.keogram;
    // create a new image
    img = gaiaViewer.windowlist.viewer.document.createElement('img');
    img.width = gaiaViewer.detailedViewModeData.keogramWidth;
    img.height = gaiaViewer.detailedViewModeData.keogramHeight;
    img.border = gaiaViewer.detailedViewModeData.border;
    img.onclick = gaiaViewer.makeCallbackForOverviewKeogram(chan);

    // replace
    oldImg.parentNode.replaceChild(img, oldImg);
    gaiaViewer.detailedViewModeData.keogram = img;
    
    // Initiate mirroring
    var mir = new gaiaMirror.gaiaMirror(src, {"nodes": nodes});  
    mir.setMakeVisible(2);
    mir.init(img);
    
    gaiaViewer.detailedViewModeData.src = src;
  }

  // reposition the time line
  var x = ((gaiaViewer.state.time.valueOf() 
	    - gaiaDate.getUTCStartOfDay(gaiaViewer.state.time).valueOf())
	   / 86400e3) * gaiaViewer.detailedViewModeData.keogramWidth
  + gaiaViewer.detailedViewModeData.border;

  // compensate for width of timeline
  x -= (gaiaViewer.detailedViewModeData.timeLine.width / 2);
  gaiaViewer.detailedViewModeData.timeLine.style.left = x + "px";
  return true;
};


gaiaViewer.makeCallbackForDetailedViewThumbnail = function(t) {
  return function () { gaiaViewer.updateState({"time": t});
		       return true; };
};


// Create a query string encoding the current state. Leading '?' is
// not outputted.
gaiaViewer.stateToQueryString = function () {
  var obj = new Object;
  obj.time = gaiaViewer.state.time.UTCstrftime('%Y%m%d%H%M%S');
  obj.mode = gaiaViewer.state.mode;

  // Single gaiaWS objects, store the database ID
  var stateVars = ["palette", "orientation", "channelOrder"]
  for (var i = 0; i < stateVars.length; ++i) {
    var name = stateVars[i];
    obj[name] = gaiaViewer.state[name].getDbid();
  }
  
  // Arrays of objects, join the database IDs into one string
  var a = new Array;
  for (var i = 0; i < gaiaViewer.state.channels.length; ++i)
    a.push(gaiaViewer.state.channels[i].getDbid());
  obj.channels = a.join(',');

  return gaia.createQueryString(obj);
};




gaiaViewer.stateCookieName = "viewerState";
gaiaViewer.setStateCookie = function ( ) {
  var exp = new Date(Date.UTC(2020, 1, 1));
  document.cookie = gaiaViewer.sprintf("%s=%s; expires=%s; path=/",
				       gaiaViewer.stateCookieName,
				       escape(gaiaViewer.stateToQueryString()),
				       exp.toGMTString());
};

// Get an object whihc can be passed to updateState from any cookies
// and query parameters. Query parameters take precendence.

gaiaViewer.getRestoreState = function (callUpdateState, inputState) {
  var savedState = new Object;
  
  // update from a cookie (if it exists)
  var cookies = gaia.unwrapCookies();
  if (cookies && cookies[gaiaViewer.stateCookieName])
    savedState 
      = gaia.getQueryParameters(cookies[gaiaViewer.stateCookieName]);
  
  // update from the URL, if it has any query parameters
  savedState = gaia.getQueryParameters(document.location.search.substr(1), 
				       savedState);
  
  // Now convert the saved state which used database IDs to one
  // which contains objects
  var state;
  var tmp;
  // if (inputState && inputState.constructor == Object)
  if (typeof inputState == "object")
    state = gaia.clone(inputState);
  else
    state = new Object;
    
  // if (savedState.hasOwnProperty("time")) {
  if (savedState.time) {
    var s = savedState.time;
    if (gaia.isArray(s))
      s = s[0];

    state.time = new Date(Date.UTC(parseInt(s.substr(0,4)),
				   parseInt(s.substr(4,2)) -1,
				   parseInt(s.substr(6,2)),
				   parseInt(s.substr(8,2)),
				   parseInt(s.substr(10,2)),
				   parseInt(s.substr(12,2))));
  }

  // if (savedState.hasOwnProperty("palette")) {
  if (savedState.palette) {
    // if (savedState.palette.constructor == Array)
    if (savedState.palette.splice)
      // An array
      tmp = gaiaWS.getPaletteByDbid(savedState.palette.pop());
    else
      tmp = gaiaWS.getPaletteByDbid(savedState.palette);
    if (tmp)
      state.palette = tmp;
  }

  if (savedState.orientation)
    state.orientation = gaiaWS.getOrientationByDbid(savedState.orientation);

  if (savedState.channelOrder)
    state.channelOrder 
      = gaiaWS.getChannelOrderByDbid(savedState.channelOrder);
    
  if (savedState.channels) {
    // if (savedState.channels.constructor == Array) {
    if (savedState.channels.splice) {
      // defined in multiple places
      savedState.channels = savedState.channels.join(',');
    }

    state.channels = new Array;
    var c = savedState.channels.split(',');
    for (var i = 0; i < c.length; ++i)
      state.channels.push(gaiaWS.getChannelByDbid(c[i]));
  }
  
  switch (savedState.mode) {
    case "overview":
    case "detailedView":
    state.mode = savedState.mode;
    break;
    // Anything else should be ignored
  }
  
  if (savedState.debug)
    gaia.debug = (savedState.debug ? true : false);
  
  return state;
};


gaiaViewer.prefetchDataAvailability = function (delay) {
  if (!gaiaWS.inOnlineMode())
    return false;

  if (delay) {
    return setTimeout(function () { gaiaViewer.prefetchDataAvailability(0); },
		      delay);
  }
  
  // Get the data availability for the two adjacent days
  var timeVal = gaiaDate.getUTCStartOfDay(gaiaViewer.state.time).valueOf();
  var prevTimeVal 
  = gaiaDate.getUTCStartOfDay(gaiaViewer.previousState.time).valueOf();
  var offsets = [-86400e3, +86400e3];
  
  for (var i = 0; i < offsets.length; ++i) {
    var t = timeVal + offsets[i];
    if (t != prevTimeVal) {
      var url = (new Date(t)).UTCstrftime(gaiaWS.webServiceBaseUrl +
					  '/data_availability/%Y/%m/%d');
      // Make asynchronous request. Don't need a handler.
      var xmlhttp =  new XMLHttpRequest();
      xmlhttp.open('GET', url, true); // asynchronous
      xmlhttp.send(null);
    }
  }

  return true;
};


// Initialise information. Only place functions in here which depend
// on the JavaScript code being ready to run. Place code which
// updates HTML objects a function called by the page's onLoad()
// handler. Note that the gaiaViewer callbacks must be registered
// BEFORE any other JavaScript files register their callbacks (eg,
// so that data availability can be updated by our callback before
// any other callbacks which may need it are run): this is why the
// callbacks below cannot be placed in onLoad().
gaiaViewer.init = function () {
  if (gaiaWS.config.blank_image && gaiaMirror)
    if (gaiaWS.inOnlineMode())
      gaiaMirror.missingImageUrl = gaiaWS.config.blank_image;
    else
      gaiaMirror.missingImageUrl = gaiaWS.getDocumentRoot() +
	gaiaWS.config.blank_image;

  gaiaViewer.state.channels = gaiaWS.data.channels; // All channels
  gaiaViewer.state.selectedChannel = gaiaViewer.state.channels[0];
  gaiaViewer.state.palette = gaiaWS.data.palettes[0];
  gaiaViewer.state.orientation = gaiaWS.data.orientations[0];
  gaiaViewer.state.channelOrder = gaiaWS.data.channelOrders[0];
  
  // update the initial state, but don't call updateState() as the
  // callbacks are yet registered
  gaiaViewer.state = gaiaViewer.getRestoreState(false, gaiaViewer.state);

  gaiaViewer.dataAvailabilityCB();

  gaiaViewer.previousState = gaia.clone(gaiaViewer.state);
  
  gaiaViewer.registerCallback(window,
			      gaiaViewer.dataAvailabilityCB, [ "date" ],
			      "gaiaViewer.dataAvailabilityCB");

  // Prevent multiple initialisiation
  gaiaViewer.init = function () { return false; };

  return true;
};
// gaiaViewer.init();

  

