/*
 * Globals: 
 */

var objMap = null;      // Google Maps object
var locCtl = null;      // Displays cursor location
var portCtl = null;     // Displays port list, handles select event
var chartCtl = null;    // Displays chart list, handles select event
var opacityCtl = null;  // Allows opacity of charts to be changed
var objTileLayer = [];  // List of tile layers visible
var objAis = null;      // The AISParser object bound to GEvent
var AisHistory = [];    // array of last message received by MMID
var objDftIcon = null;  // Essentially a GIcon but modified for AIS.
var objStaIcon = null;  // Essentially a GIcon but modified for AIS.


// Time of initial load
var t0 = new Date();




/*
 * @public
 * Overrides the default toString method with something more informative.
 * Ignores functions
 * @return string representation of the contents of the generic object.
 */
Object.prototype.toString = function() { 
  var props = [];
  for(var i in this) {
    // escape the index for string representation
    var i_esc = (isNaN(i)) ? "'"+i+"'" : i;
    
    // don't show functions in toString()
    if( (typeof(this[i])=="function") ) { 
      continue;
    }
    
    // handle Non-numerics differently
    if( isNaN(this[i]) ) {
    // not a number
      if( (typeof(this[i])=="string") ) { 
      // clean string to be stored as "<string>"
        var str = this[i].toString();
        str = str.replace(/"/g,'\\"');
        props.push(i_esc + ':"' + str + '"');
      } else { 
      // let object override provide toString() function
        props.push(i_esc + ':' + this[i].toString());
      }
    } else { 
    // store as number
      props.push(i_esc + ':' + this[i]);
    }
  }
  return '{'+props.join(",")+"}";
};


function StationIcon(copy,url) { 
  copy = copy || G_DEFAULT_ICON;
  url = url || "./ico/base.png";
  GIcon.call(this,copy,url);
  this.shadow = "./ico/base_s.png";
  this.iconSize = new GSize(16,16);
  this.shadowSize = new GSize(22,16);
  this.iconAnchor = new GPoint(8,11);
  this.infoWindowAnchor = new GPoint(4,7);
  this.printImage = null;
  this.mozPrintImage = null;
  this.printShadow = null;
  this.transparent = null;
  return this;
}
StationIcon.prototype = new GIcon;


function ShipIcon(copy,url) {
  copy = copy || G_DEFAULT_ICON;
  url = url || "./lib/img/blank.gif";
  GIcon.call(this,copy,url);
  this.shadow = null;
  this.iconSize = new GSize(21,21);
  this.shadowSize = null;
  this.iconAnchor = new GPoint(11,11);
  this.infoWindowAnchor = new GPoint(11,11);
  this.printImage = null;
  this.mozPrintImage = null;
  this.printShadow = null;
  this.transparent = null;
  return this;
}
ShipIcon.prototype = new GIcon;


function getShipTypeAndCargo(ix) { 
  if( ix < 20 || ix > 99 ) { 
    return (ix==10) ? "Fishing" : null;
  }
  
  var category = [null,'Reserved','WIG','Vessel','HSC',null,
    'Passenger','Cargo','Tanker','Other'];

  var cargo = [null,'Hazard/Cat. A','Hazard/Cat. B','Hazard/Cat. C',
    'Hazard/Cat. D',null,null,null,null,null];

  var cat3 = ["Fishing","Towing","Towing","Dredger","Dive",
    "Military Operations","Sailing","Pleasure Craft",null,null];

  var cat5 = ["Pilot Vessel","Search and Rescue","Tug","Port Tender",
   "Anti-Pollution","Law Enforcement","Medical Transport",
   "RR Resolution No. 18 (Mob-83) Type"];


  var tens = Math.floor(ix/10);
  var ones = ix%10;

  if( tens == 3 ) { 
    return cat3[ones];

  } else if( tens == 5 ) { 
    return cat5[ones];

  } else { 
    var cat = category[tens];
    var car = cargo[ones];
    if( cat ) { 
      if( car ) { 
        return cat + ' carrying ' + car;
      } else { 
        return cat;
      }
    } else { 
      if( car ) {
        return car;
      } else {
        return "Unknown";
      }
    }
    return "Unknown";
  }

  return '';
}


/*
 * Executed whenever the map's viewport changes
 */
function doMoveEnd() { 
  if( chartCtl.isLoaded() ) { 
    chartCtl.show(objMap.getBounds());
  }
  // doViewportChanged();
}


function doSingleRightClick(point,src,overlay) { 
  var latlng = objMap.fromContainerPixelToLatLng(point);
  var panels = chartCtl.listContainsLatLng(latlng);
  var menu = buildContextMenu(panels);
  var menu_w = parseInt(menu.style["width"]);

  var div = document.getElementById("contextmenu");
  div.innerHTML = "";
  div.appendChild(menu);
  
  var cof = objMap.getContainerOffset();
  var cos = objMap.getSize();
  div.style.position = "absolute";
  div.style.top = (cof.y + point.y - 3)+"px";

  if( point.x >= (cos.width-menu_w) ) { 
    // if click point + width of menu wouuld extend beyond map viewport
    // display menu to left of the mouse
    div.style.left = (cof.x + point.x - 10 - menu_w)+"px";
  } else {
    // else display menu to the right of the mouse
    div.style.left = (cof.x + point.x + 13)+"px";
  }
  div.style.display = "block";
}


function buildContextMenu(panels) { 
  // create the menu div
  var d0 = document.createElement("div");
  d0.style["margin"] = "0.2em 0.4em";
  d0.style["backgroundColor"] = "white";
  d0.style["border"] = "solid #aaa 1px";
  d0.style["margin"] = "0";
  d0.style["padding"] = "1px 0";
  d0.style["width"] = "170px";

  
  // create the header space
  var d1 = document.createElement("div");
  d1.style["fontSize"] = "1.1em";
  d1.style["fontWeight"] = "bold";
  d1.style["paddingLeft"] = "3px";

   // create the close button
   var im = document.createElement("img");
   im.setAttribute("align","right");
   im.setAttribute("border",0);
   im.setAttribute("height",13);
   im.setAttribute("src","lib/img/close.gif");
   im.setAttribute("valign","middle");
   im.setAttribute("width",14);
   im.setAttribute("vspace",2);
   im.style["margin"] = "0";
   im.style["padding"] = "2px";
   im.onmouseover = function() { 
     this.style["backgroundColor"] = "#ccc";
   };
   im.onmouseout = function() { 
     this.style["backgroundColor"] = "transparent";
   };
   im.onclick = function() {
     hide('contextmenu');
     return false; 
   };
   d1.appendChild(im);

   // create the text node
   d1.appendChild(document.createTextNode("Panel List"));

  d0.appendChild(d1);

  // create the list element
  var ul = document.createElement("ul");
  ul.style["margin"] = "0";
  ul.style["padding"] = "0";
  ul.style["listStyle"] = "none";

  for(var i in panels) { 
    var id = String(panels[i].file_name).split('\.')[0];
    var li = document.createElement("li");
    li.style["fontSize"] = "10px";
    li.style["lineHeight"] = "20px";
    li.style["margin"] = "0";
    li.style["padding"] = "0 3px";
    li.style["verticalAlign"] = "middle";
    li.style["cursor"] = "pointer";
    li.title = panels[i].title;

    var isChecked = false;
    if(typeof(objTileLayer[id]) != "undefined" ) { 
      isChecked = true;
    }

    // create IE-style input checkbox
    if( navigator.appVersion.indexOf("MSIE") >= 0 ) { 
      var chk = (isChecked) ? ' checked="checked"' : '';
      var ch = document.createElement('<input type="checkbox"'+chk+' />');
    } else { 
      // create DOM compliant input checkbox
      var ch = document.createElement("input");
      ch.setAttribute("type","checkbox");
      ch.checked = isChecked;
    }

    // note the kap panel as the value of this checkbox
    ch.value = id;

    // IE returns state change on blur.
    ch.onclick  = function() { doKapLayerChanged(this); }

    ch.style["marginLeft"] = "0";
    ch.style["padding"] = "0";
    ch.style["cssFloat"] = "left";
    ch.style["styleFloat"] = "left";
    li.appendChild(ch);
    ch = null;

    // create a scale span
    var sc = document.createElement("span");
    sc.style["clear"] = "right";
    sc.style["cssFloat"] = "right";
    sc.style["styleFloat"] = "right";
    sc.style["textAlign"] = "right";
    sc.title = "Panel Scale [1:N]";
    sc.appendChild(document.createTextNode(panels[i].scale));
    li.appendChild(sc);
    sc = null;

    // append text: kap file_name
    li.appendChild(document.createTextNode(" "+id));
    li.polygon = panels[i].getPolygon();

    li.onmouseover = function() { 
      this.style["backgroundColor"] = "#ffeac0";
      objMap.addOverlay(this.polygon);
    }
    li.onmouseout = function() { 
      this.style["backgroundColor"] = "transparent";
      objMap.removeOverlay(this.polygon);
    }
    li.onclick = function(e) { 
      e = (!e) ? event : e;
      var targ = (e.target) ? e.target : e.srcElement;
      if( String(targ.tagName).toUpperCase() == "LI" ) { 
        targ.getElementsByTagName("input")[0].click();
      }
    };
    ul.appendChild(li);
  }

  var n = Math.min(6,panels.length);
  if( n < 1 ) { 
    var li = document.createElement("li");
    var ii = document.createElement("span");
    ii.style["fontStyle"] = "oblique";
    ii.appendChild(document.createTextNode("No panels here."));
    li.appendChild(ii);

    li.style["fontSize"] = "10px";
    li.style["lineHeight"] = "20px";
    li.style["margin"] = "0";
    li.style["padding"] = "0 3px";
    li.style["verticalAlign"] = "middle";
    li.onmouseover = function() { 
      this.style["backgroundColor"] = "#ffeac0";
    }
    li.onmouseout = function() { 
      this.style["backgroundColor"] = "transparent";
    }

    ul.appendChild(li);
    n = 1;
  }
  var lh = parseInt(li.style["lineHeight"]);
  if( navigator.appVersion.indexOf("MSIE") >= 0 ) { 
    ul.style["height"] = ((n*lh)+2)+"px";
  } else { 
    ul.style["height"] = (n*lh)+"px";
  }
  ul.style["overflow"] = "auto";

  // append list element
  d0.appendChild(ul);

  // return menu
  return d0;
} // end: buildContextMenu(panels)


function show(elid) { 
  document.getElementById(elid).style["display"] = "block";
}

function hide(elid) { 
  document.getElementById(elid).style["display"] = "none";
}



function doKapLayerChanged(checkbox) { 
  var kap_id = checkbox.value;
  if( checkbox.checked ) { 
    addOverlayById(kap_id);
  } else { 
    removeOverlayById(kap_id);
  }
}



/*
 * Executed when menu item is selected
 */
function doActivateChart(idx) { 

  var obj = chartCtl.getSelectedValue();

  // remove previous overlays
  for(var id in objTileLayer) { 
    removeOverlayById(id);
  }

  // reset layer array & opacity manager
  // objTileLayer = [];
  // opacityCtl.clearOverlays();

  if( !obj.panels || obj.panels.length < 1) { 
    return false;
  }

  // add new overlays
  for(var k in obj.panels) { 
    if( !obj.panels[k].isHidden ) { 
      var id = String(obj.panels[k].file_name).split('\.')[0];
      addOverlayById(id);
    }
  }
  return true;
}


/*
 * New port selected: 
 * Reset the chart control
 * Select a good chart if available
 */
function doActivatePort(idx) { 
  for(var id in objTileLayer) { 
    removeOverlayById(id);
  }
  chartCtl.clearSelected();
}


function addOverlayById(id) { 
  if( typeof(objTileLayer[id]) != "undefined" ) { 
    return false;
  }
  objTileLayer[id] = new KapTileLayerOverlay(id);
  opacityCtl.addOverlay(objTileLayer[id]);
  objMap.addOverlay(objTileLayer[id]);
  return true;
}


function removeOverlayById(id) { 
  if( typeof(objTileLayer[id]) != "undefined" ) { 
    // GEvent.clearNode(objTileLayer[id]);
    objMap.removeOverlay(objTileLayer[id]);
    opacityCtl.removeOverlay(objTileLayer[id]);
    delete(objTileLayer[id]);
    return true;
  }
  return false;
}




/*
 * Purpose: display panels that are fully visible.
 *  Mark most visible panel(s) for display, hide remainder.
 */
function doViewportChanged() { 
  // currently selected chart set
  var obj = chartCtl.getSelectedValue();
  if( !obj ) { 
    return;
  }

  // viewport bounds & area
  var mb  = objMap.getBounds();
  var ma = mb.getArea();

  // calculate percent of chart visible in view pane
  var panel_pct = [];
  for(var k in obj.panels) { 
    /* Conditions:
     *  1) viewport does not contain panel (0%)
     *  2) viewport fully covered by panel (100%)
     *  3) viewport partially covered by panel (n%)
     */
    var pb = obj.panels[k].getBounds();
    var ob = mb.getOverlapBounds(pb);
    var pct = (!ob) ? 0 : ob.getArea()/ma;
    panel_pct.push({'idx':i,'pct':pct});
  }

  // sort descending
  panel_pct.sort(function(a,b) { return (b['pct']-a['pct']); });
  var ctr = 0;
  for(var k in panel_pct) { 
    var idx = panel_pct[k]['idx'];
    var pct = panel_pct[k]['pct'];
    var pan = obj.panels[idx];
    if( pct > 0.02 || ctr < 1 ) { 
      pan.show();
    } else { 
      pan.hide();
    }
    ctr++;
  }
  // doActivateChart(chartCtl.getSelectedIndex());
}



function num_pad(num,left,right,ch) { 
  var sign = (num<0) ? '-' : '';
  var pts = String(Math.abs(num)).split("\.");
  ch = (!ch) ? '0' : ch;

  // handle the whole number portion
  var delta = (left  - pts[0].length);
  if( delta>=0 ) { 
    // needs padding
    for(var i=0;i<delta;i++) { 
      pts[0] = ch + '' + pts[0];
    }
  }
  // handle fractional portion
  delta = (right - pts[1].length);
  if( delta >= 0 ) { 
    for(var i=0;i<delta;i++) { 
      pts[1] = pts[1] + '' + ch;
    }
  } else { 
    pts[1] = pts[1].substr(0,right);
  }
  return sign+pts.join('.');
}


function evt_ais_invalid(message) { 
  var dst = document.getElementById("historyAis");
  var tables = dst.innerHTML.split("</table>");
  dst.innerHTML = '<table border="1"><tr><th>Invalid/Unsupported:</th><td>'+
    htmlentities(message)+'</tr></table>';
  for(var i=0,n=Math.min(5,tables.length); i<n; i++) { 
    dst.innerHTML += tables[i]+"</table>";
  }
}

function evt_ais_checksum(aism) { 
  var dst = document.getElementById("historyAis");
  var tables = dst.innerHTML.split("</table>");
  dst.innerHTML = '<table border="1"><tr><th>Checksum Error:</th><td>'+
    htmlentities(aism.message)+'</tr></table>';
  for(var i=0,n=Math.min(5,tables.length); i<n; i++) { 
    dst.innerHTML += tables[i]+"</table>";
  }
}

function evt_ais_error(aism) {
  var dst = document.getElementById("historyAis");
  var tables = dst.innerHTML.split("</table>");
  dst.innerHTML = '<table border="1"><tr><th>Decode Error:</th><td>'+
    htmlentities(aism.message)+'</tr></table>';
  for(var i=0,n=Math.min(5,tables.length); i<n; i++) { 
    dst.innerHTML += tables[i]+"</table>";
  }
}
function evt_ais_unexpected(aism) { 
  var dst = document.getElementById("historyAis");
  var tables = dst.innerHTML.split("</table>");
  dst.innerHTML = '<table border="1"><tr><th>Unexpected Error:</th><td>'+
    htmlentities(aism.message)+'</tr></table>';
  for(var i=0,n=Math.min(5,tables.length); i<n; i++) { 
    dst.innerHTML += tables[i]+"</table>";
  }
}

function evt_map_click(marker,point) { 
  if( !marker || !marker["userid"] ) { 
    return;
  }
  var userid = marker["userid"];
  var ref = AisHistory[userid];

  var html = '<span class="hdr">MMSI: '+userid+'</span>';
  if( !( ref["latitude"] == null || ref["longitude"] == null) ) { 
    html += '<b>Lat:</b> '+ref["latitude"]+
    '<br/><b>Lon:</b> '+ref["longitude"];
  }
  if( ref["heading"] != null ) { 
    html += '<br/><b>Heading:</b> '+(ref["heading"])+'&deg;N';
  }
  if( ref["cog"] != null ) { 
    html += '<br/><b>Course:</b> '+(ref["cog"])+'&deg;N';
  }
  if( ref["sog"] != null ) { 
    html += '<br/><b>Speed:</b> '+(ref["sog"])+' knots';
  }
  if( ref["rot"] != null ) { 
    html += '<br/><b>Turn Rate:</b> '+(ref["rot"])+'&deg;/min';
  }

  if( typeof(ref["dstid"]) != "undefined" ) { 
    var dst = AisHistory[ref["dstid"]];
    if( typeof(dst["name"]) != "undefined" ) { 
      html += '<br/><b>Destination:</b> '+(dst["name"]);
    }
  }

  if( typeof(ref["static"]) != "undefined" ) { 
    if( ref["static"]["imo_num"] ) { 
      html += "<br/><b>IMO:</b> "+(ref["static"]["imo_num"]);
    }
    if( ref["static"]["callsign"] ) { 
      html += "<br/><b>Call Sign:</b> "+(ref["static"]["callsign"]);
    }
    if( ref["static"]["name"] ) { 
      html += "<br/><b>Name:</b> "+(ref["static"]["name"]);
    }
    if( ref["static"]["ship_type"] ) { 
      var text = getShipTypeAndCargo(ref["static"]["ship_type"]);
      if( text ) { 
        html += "<br/><b>Ship/Cargo:</b> "+text;
      }
    }
    if( typeof(ref["static"]["destination"]) ) { 
      html += "<br/><b>Destination:</b> "+(ref["static"]["destination"]);
    }
  }


  var maxContent = '<h3>MMSI: '+userid+'</h3>';
  for(var k in ref) { 
    if( k.indexOf("g_") >= 0 ) { 
      continue;
    }
    maxContent += '<br/><b>'+k+':</b> '+ref[k];
  }

  if( !marker.openInfoWindowHtml ) { 
    return;
  }
  marker.openInfoWindowHtml(
    '<div id="infoWin">'+html+'</div>',
    {"maxContent":maxContent}
  );
}


function initAisObjects() { 
  objDftIcon = new ShipIcon();
  objStaIcon = new StationIcon();
  objAis = new AISParser(GEvent);

  GEvent.addListener(objAis,"complete",evt_ais_complete);
  GEvent.addListener(objAis,"decode_unexpected",evt_ais_unexpected);
  // GEvent.addListener(objAis,"decode_error",evt_ais_error);
  GEvent.addListener(objAis,"invalid_checksum",evt_ais_checksum);
  // GEvent.addListener(objAis,"invalid_message",evt_ais_invalid);
  GEvent.addListener(objMap,"click",evt_map_click);


  // q();
}

function doStart() { 
  q();
}

function doStop() { 
  var old = document.getElementById('inlineStreamer');
  if (old) {
    old.src = "about:blank";
    old.parentNode.removeChild(old);
  }
}
  
function d(msg,ts) { 
  dispatchAisMessage(msg,ts);
}
function q(){ 
  // initializeAisScriptStream();
  initializeAisIframeStream();
}

function initializeAisIframeStream() { 
  var old = document.getElementById('inlineStreamer');
  if (old) old.parentNode.removeChild(old);
  script = document.createElement('iframe');
  script.style.display = "none";
  script.src = 'dat/relay_script.php?limit=200';
  script.id = 'inlineStreamer';
  (document.getElementsByTagName("body"))[0].appendChild(script);
}

// A better way, but not real-time
function initializeAisScriptStream() { 
  var old = document.getElementById('inlineStreamer');
  if (old) old.parentNode.removeChild(old);
  script = document.createElement('script');
  script.src = 'dat/relay_script.php?limit=250';
  script.type = 'text/javascript';
  script.defer = true;
  script.id = 'inlineStreamer';
  (document.getElementsByTagName("head"))[0].appendChild(script);
}


function evt_ais_complete(nmea_code, ais_obj, ais_msg) {
  var ais_msgid = ais_obj["msgid"];

  switch(ais_msgid) { 
    case 1:
    case 2:
    case 3:
      doShipMessage(ais_obj);
      break;
    case 5:
      doStaticMessage(ais_obj);
      break;
    case 4:
    case 11:
      doStationMessage(ais_obj);
      break;
    case 10:
      var srcid = ais_obj["srcid"];
      if( typeof(AisHistory[srcid]) == "undefined" ) {
        AisHistory[srcid] = [];
      }
      AisHistory[srcid]["dstid"] = ais_obj["dstid"];
      break;
    case 15: 
      var srcid = ais_obj["srcid"];
      if( typeof(AisHistory[srcid]) == "undefined" ) {
        AisHistory[srcid] = [];
      }
      AisHistory[srcid]["dstid"] = ais_obj["dstid_1"];
      break;
    default:
  }
}


function doShipMessage(ais_obj) { 
  var userid = ais_obj['userid'];

  if(typeof(AisHistory[userid]) == "undefined" ) { 
    AisHistory[userid] = [];
  }

  if( typeof(AisHistory[userid]["g_marker"]) == "undefined" ) { 
    AisHistory[userid]["g_marker"] = new GMarker(
      new GLatLng(0,0), {icon:objDftIcon,clickable:true}
    );

    AisHistory[userid]["g_marker"]["userid"] = userid;
    objMap.addOverlay(AisHistory[userid]["g_marker"]);
  }

  if( typeof(AisHistory[userid]["g_polyline"]) == "undefined" ) { 
    // ship track
    AisHistory[userid]["g_polyline"] = new GPolyline(
      [],"#402080",2,0.8,{"geodesic":true}
    );
    AisHistory[userid]["g_polyline"]["userid"] = userid;
    objMap.addOverlay(AisHistory[userid]["g_polyline"]);
  }

  // store remaining attributes
  for(var k in ais_obj) { 
    AisHistory[userid][k] = ais_obj[k];
  }
  // update map, track and icon
  updateMap(AisHistory[userid]);
}


function doStationMessage(ais_obj) { 
  var userid = ais_obj['userid'];

  if(typeof(AisHistory[userid]) == "undefined" ) { 
    AisHistory[userid] = [];
  }

  if( typeof(AisHistory[userid]["g_marker"]) == "undefined" ) { 
    AisHistory[userid]["g_marker"] = new GMarker(
      new GLatLng(0,0), {icon:objStaIcon,clickable:true}
    );

    AisHistory[userid]["g_marker"]["userid"] = userid;
    objMap.addOverlay(AisHistory[userid]["g_marker"]);
  }

  if( typeof(AisHistory[userid]["g_polyline"]) == "undefined" ) { 
    // base station track ( TODO: remove after verified)
    AisHistory[userid]["g_polyline"] = new GPolyline(
      [],"#402080",2,0.8,{"geodesic":true}
    );
    AisHistory[userid]["g_polyline"]["userid"] = userid;
    objMap.addOverlay(AisHistory[userid]["g_polyline"]);
  }

  // store remaining attributes
  for(var k in ais_obj) { 
    AisHistory[userid][k] = ais_obj[k];
  }
  // update map, track and icon
  updateMap(AisHistory[userid]);
}


function doStaticMessage(ais_obj) { 
  var userid = ais_obj['userid'];

  if(typeof(AisHistory[userid]) == "undefined" ) { 
    AisHistory[userid] = [];
  }

  AisHistory[userid]["static"] = ais_obj;
}

function updateMap(last_obj) { 
  if( last_obj["latitude"]  == null || 
      last_obj["longitude"] == null ){
    return;
  }

  // get location
  var latlng = new GLatLng(
    parseFloat(last_obj["latitude"]),
    parseFloat(last_obj["longitude"])
  );

  if( last_obj["g_marker"].getIcon() == objStaIcon ) { 
  // If this item happens to be a station, just update location
    last_obj["g_marker"].setLatLng(latlng);

  } else { 
  // else update pointer too

    // update polyline
    if( typeof(last_obj["g_polyline"]) != "undefined" ) { 
      var n = last_obj["g_polyline"].getVertexCount();
      last_obj["g_polyline"].insertVertex(n,latlng);
    }

    // update marker icon
    var cog = Math.round(last_obj["cog"]);
    cog = (cog<10) ? '00'+cog : ((cog<100) ? '0'+cog : cog);
    last_obj["g_marker"].setImage("./ico/ptr_"+cog+".png");
    last_obj["g_marker"].setLatLng(latlng);
  }
}

function doValidHtml(ais_obj) { 
  var keys = [];
  var header = [];
  var ctr = 0;
  for(var k in ais_obj) { 
    if( ais_obj[k] == null ) { 
      continue;
    }
    keys.push(k);
    header.push(htmlentities(k));
    if( ++ctr >= 12 ) { 
      break;
    }
  }

  var html = '<table border="1"><tr><th>';
  html += header.join("</th><th>");
  html +='</th></tr><tr>';

  ctr = 0;
  for(var i in keys) { 
    var k = keys[i];
    html += '<td>'+htmlentities(ais_obj[k].toString())+'</td>';
    if( ++ctr >= header.length ) { 
      break;
    }
  }
  html += '</tr></table>';

  var dst = document.getElementById("historyAis");
  var tables = dst.innerHTML.split("</table>");
  dst.innerHTML = html;
  for(var i=0,n=Math.min(5,tables.length); i<n; i++) { 
    dst.innerHTML += tables[i]+"</table>";
  }
}


function htmlentities(txt) { 
  txt = String(txt).replace(/&/g,'&amp;');
  txt = txt.replace(/</g,'&lt;');
  txt = txt.replace(/>/g,'&gt;');
  return txt;
}

function dispatchAisMessage(msg,ts) { 
  if( ts ) { 
    var html ='<span id="latestAisTime">'+ts+'</span> ' + htmlentities(msg);
    document.getElementById("latestAis").innerHTML = html;
  } else { 
    document.getElementById("latestAis").innerHTML = htmlentities(msg);
  }
  objAis.receive(msg);
}


function init() { 
  var el = document.getElementById("Gmap"); 
  if(!el) {
    alert("This page does not contain a Gmap element");
    return;
  }
  if( !GBrowserIsCompatible() ) { 
    alert("Your browser does not support Google Maps v2");
    return false;
  }

  // Initialize the map object
  objMap = new GMap2(el);

  // Initialize the location control
  locCtl = new GLocationControl(null,90);

  // Initialize the charts control
  chartCtl = new GChartControl();
  // chartCtl.load();

  // Initialize the ports control
  portCtl = new GPortControl();
  // portCtl.load();

  // Initialize the opacity control
  opacityCtl = new GOpacityControl(0.70);


  GEvent.addListener(objMap,"load",function() { 
    chartCtl.load();
    portCtl.load();
  });


  GEvent.addListener(chartCtl,"load",function() { 
    if( !objMap.isLoaded() ) { 
      return;
    }
    chartCtl.show(objMap.getBounds());
  });

  // show the chart selected, jump to view
  GEvent.addListener(chartCtl,"itemselected",doActivateChart);

  // reset chart control, jump to port, select port chart
  GEvent.addListener(portCtl,"itemselected",doActivatePort);

  // repopulate the chart control with visible charts
  // optional: select best chart for view (if auto-view mode)
  GEvent.addListener(objMap,"moveend",doMoveEnd);

  // show context menu (list panels for location clicked)
  GEvent.addListener(objMap,"singlerightclick",doSingleRightClick);

  // hide right-click menu when map regains focus
  GEvent.addListener( objMap, "click", function(){
    hide('contextmenu');
  });
  GEvent.addDomListener( objMap.getContainer(), "mousedown", function(){
    hide('contextmenu');
  });


  // Add some baselayers
  objMap.addMapType(G_PHYSICAL_MAP);

  // Select default map type
  objMap.setMapType(G_NORMAL_MAP);

  // Center center and zoom
  objMap.setCenter(new GLatLng(33.0,-119.0),6);

  objMap.addControl(
    new GMenuMapTypeControl(),
    new GControlPosition(G_ANCHOR_TOP_RIGHT,new GSize(5,55))
  );

  objMap.addControl(
    portCtl,
    new GControlPosition(G_ANCHOR_TOP_RIGHT,new GSize(5,30))
  );

  objMap.addControl(
    chartCtl,
    new GControlPosition(G_ANCHOR_TOP_RIGHT,new GSize(5,5))
  );




  objMap.addControl(new GLargeMapControl());
  objMap.addControl(
    opacityCtl,
    new GControlPosition(G_ANCHOR_BOTTOM_RIGHT, new GSize(7, 38))
  );

  objMap.addControl(locCtl);


  objMap.addControl(
    new GScaleControl(),
    new GControlPosition(G_ANCHOR_BOTTOM_LEFT, new GSize(5, 35))
  );

  // Enable Google Search on map
  objMap.enableGoogleBar();


  objMap.addControl(
    new DragZoomControl({},{},{}),
    new GControlPosition(G_ANCHOR_TOP_LEFT, new GSize(1, 1))
  );

  // Display a status message (loaded in xyz.zzz seconds)
  el = document.getElementById("status");
  if( el ) { 
    var t1 = new Date();
    el.innerHTML = "Document loaded ("+(t1.getTime()-t0.getTime())/1000+" sec)";
  }

  // if( navigator.appVersion.indexOf("MSIE") < 0 ) { 
    initAisObjects();
  // }
}


window.onload = init;
window.onunload = GUnload;
