// BUGS:
// DELETE DOES NOT REMOVE OVERLAY MASK
// WHY DO WE BOTHER USING THE GPOLYLINE WHEN LineOverlay does all we need?

//----------------------------------------------------------------------
var userAgent = self.navigator.userAgent;
var isMsie = userAgent.indexOf('MSIE')>0;
var featureUid = 0;
var currentZoom = 10;

function formatFloat(value, places)
{
    var f = Math.pow(10,places);
    return parseInt(value*f)/f;
}

function xmlEntities(str) {

    if (str) {
	var escAmpRegEx = /&/g;
	var escLtRegEx = /</g;
	var escGtRegEx = />/g;
	var quotRegEx = /"/g;
        var aposRegEx = /'/g;
        
        str = str.replace(escAmpRegEx, "&amp;");
        str = str.replace(escLtRegEx, "&lt;");
        str = str.replace(escGtRegEx, "&gt;");
        str = str.replace(quotRegEx, "&quot;");
        str = str.replace(aposRegEx, "&apos;");
    } else {
        str = "";
    }
  return str; 
}    

// I'm using this hack for double clicks
// because I could not get dblclick events
// to fire if I turned of map dragging
// by calling map.disableDragging().
// As soon as that fn is called, the dblclick
// events stop being received.  Go figure.
// Originally was just using timing events,
// but have now extended to check pixel travel
// because it was to easy to get accidental
// clicks.
var lastClickPoint;
function isDoubleClick(point)
{
    var isDouble = false;
    var pixelPoint = map.fromLatLngToDivPixel(point);
    var now = new Date().getTime();
    if (lastClickPoint!=null) {
	var delta = Math.abs(lastClickPoint.x-pixelPoint.x) + Math.abs(lastClickPoint.y-pixelPoint.y);
	var elapsed = now - lastClickTime;
	isDouble = (elapsed<500 && delta<10);  // double-click threshold
    }
    lastClickTime = now;
    lastClickPoint = pixelPoint;
    return isDouble;
}

// used to be called super() but IE doesn't like that.
function superfn(o, args)
{
    if (args.callee.superclass) {
	return args.callee.superclass.apply(o,args);
    }
}

//----------------------------------------------------------------------

//----------------------------------------------------------------------

function LayerNode() 
{
    this.uid = ++featureUid;
    //    this.attributeValues = [];
    this.attrib = []; // keys are used, values are ignored
    this.overlay = null;
}
LayerNode.prototype.className = 'node';
LayerNode.prototype.toString = function() { return "I am a feature"; };
LayerNode.prototype.getClassName = function() { return this.className; };
LayerNode.prototype.isVisible = false;
LayerNode.prototype.dirty = false;  // if true, needs to be saved

LayerNode.prototype.dispose = function LayerNode_dispose()
{
    this.hide();
    this.overlay = null;
}

LayerNode.prototype.show = function LayerNode_show()
{
    if (!this.isVisible) {
	this.addOverlay(map);
	this.isVisible = true;
    }
}
LayerNode.prototype.hide = function LayerNode_hide()
{
    var wasVisible = this.isVisible;
    if (this.isVisible) {
	this.removeOverlay(map);
	this.isVisible = false;
    }
    return wasVisible;
}

LayerNode.prototype.refresh = function LayerNode_refresh()
{
    if (this.hide()) {
	this.show();
    }
}

LayerNode.prototype.setDirty = function LayerNode_setDirty()
{
    this.dirty = true;
    // revisit - probably need to set a global flag here too,
    // and update the state of a save button?
}

LayerNode.prototype.removeOverlay = function LayerNode_removeOverlay(map)
{
    if (this.overlay) map.removeOverlay(this.overlay);
}


LayerNode.prototype.openInfoWindow = function LayerNode_openInfoWindow(latLng)
{
    var feature = this.feature;
    if (!(latLng instanceof GLatLng)) {
	latLng = this.getPoint();
    }
    log('opening window on marker at '+latLng);
	//    map.openInfoWindow(latLng, node);
    digitize.layer.schema.onCancel = function() {
	log('onCancel');
	map.closeInfoWindow();
    };
    digitize.layer.schema.onSave = function() {
	if (this.validate()) {
  	  log('onSave');
	  digitize.saveFeature(feature);
	  map.closeInfoWindow();
        }
    }
    digitize.layer.schema.onDelete = function() {
	log('onDelete');
	digitize.deleteFeature(feature);
	map.closeInfoWindow();
    }
    
    //if (digitize.layer.featureForm==null) {
    //digitize.layer.featureForm= digitize.layer.schema.getForm({deleteButton: true});
    //}
    digitize.layer.schema.setValue(feature);
    map.openInfoWindow(latLng, digitize.layer.featureForm);
}

LayerNode.prototype.getDescription = function LayerNode_getDescription()
{
    return "getDescription not implemented";
}

/*
LayerNode.prototype.getAttrib = function LayerNode_getAttribute(i)
{
    return this.attributeValues[i] || "";
}

LayerNode.prototype.setAttrib = function LayerNode_setAttrib(i, value)
{
    this.attributeValues[i]=value;
}
*/

LayerNode.prototype.setMode = function LayerNode_setMode(mode)
{
    log("unhandled setMode call");
}

LayerNode.prototype.getXmlAttributes = function LayerNode_getXmlAttribute()
{
    var xml = "";
    for (var name in this.attrib) {
	value = this[name];
	xml+=' '+name+'="'+xmlEntities(value)+'"';
    }
    return xml;
}

LayerNode.prototype.getXmlChildren = function LayerNode_getXmlChildren() {
    return "";
}

LayerNode.prototype.getXml = function LayerNode_getXml()
{
    var xml = '<'+this.className+this.getXmlAttributes()+'>';
    xml+= this.getXmlChildren();
    xml+= '</'+this.className+'>';
    return xml;
}

LayerNode.prototype.loadXmlChildren = function LayerNode_loadXmlChildren(node)
{
}

LayerNode.prototype.loadXmlAttributes = function LayerNode_loadXmlAttributes(node)
{
    for (i=0;i<node.attributes.length;i++) {
        var name=node.attributes[i].nodeName;
	var value=node.attributes[i].nodeValue;
	this[name]=value;
	this.attrib[name]=value;
    }
}

LayerNode.prototype.loadXml = function LayerNode_loadXml(node)
{
    this.loadXmlChildren(node);
    this.loadXmlAttributes(node);
}

LayerNode.prototype.findCloser = function LayerNode_findCloser(pixelPt, closest)
{
    return false;
}

/*
LayerNode.prototype.getAttributeTable = function LayerNode_getAttributeTable()
{
    var div = document.createElement('div');
    div.appendChild(document.createTextNode(this.getDescription()));

    var table = div.appendChild(document.createElement('table'));
    
    for (var i=0;i<this.attrib.length;i++) {
	var row = table.appendChild(document.createElement('tr'));
	var col1 = row.appendChild(document.createElement('td'));
	col1.appendChild(document.createTextNode(this.attributeNames[i]));
	var col2 = row.appendChild(document.createElement('td'));
	var node = document.createElement('input');
	node.setAttribute('type','text');
	node.setAttribute('class','mapinput');
	node.setAttribute('value',this.getAttrib(i));
	node.setAttribute('onKeyUp','setLayerNodeAttribute('+this.uid+','+i+',this.value);');
	if (i==0) {
	    node.setAttribute('id','firstAttribute');
	}
	col2.appendChild(node);
    }
    //    row.appendChild(document.createElement('td'));

    var a = document.createElement('a');
    a.setAttribute('class','mapaction');
    if (isMsie) {
	a.setAttribute('href',"javascript:map.closeInfoWindow();deleteLayerNode("+this.uid+");");
    } else {
	a.setAttribute('onclick',"map.closeInfoWindow();deleteLayerNode("+this.uid+");");
    }
    a.appendChild(document.createTextNode('DELETE'));
    div.appendChild(a);
    if (isMsie) {
	div.innerHTML+="&nbsp;"; // hack to make MSIE recalculate table size
    }

    // HACK
    //div.appendChild(document.createTextNode(xmlGetData()));

    return div;
}
*/

//----------------------------------------------------------------------
function Point(point) {
    superfn(this, arguments);
    this.attrib['lat']=null;  // identify required attributes
    this.attrib['lng']=null;
    this.point = point;
}
Point.superclass = LayerNode;
Point.prototype = new LayerNode();
Point.prototype.className = 'point';
Point.prototype.toString = function() { return "I am a point"; };
Point.prototype.setPoint = function Point_setPoint(gPoint) {
    this.point = gPoint;
}

Point.prototype.getXmlAttributes = function Point_getXmlAttributes() {
    var xml = ' lat="'+this.point.lat()+'" lng="'+this.point.lng()+'"';
    return xml;
}

Point.prototype.loadXml = function Point_loadXml(node)
{
    this.point = new GLatLng(parseFloat(node.getAttribute("lat")),
			     parseFloat(node.getAttribute("lng")));
    //Point.superclass.prototype.loadXml(node);  // correct but unnecessary
    this.loadXmlAttributes(node); // do this instead
}

Point.prototype.addOverlay = function Point_addOverlay(map) {
    this.overlay = new GMarker(this.point, {icon: blueDotIcon, draggable: true});
    GEvent.addListener(this.overlay, 'dragstart', function() {log('dragstart');map.closeInfoWindow();digitize.setMode('point',false);});
    GEvent.bind(this.overlay, 'dragend', this, this.dragEnd);
    GEvent.bind(this.overlay, 'click', this, this.openInfoWindow);
    map.addOverlay(this.overlay);
}

Point.prototype.getPoint = function Point_getPoint() {
    return this.point;
}

Point.prototype.getDescription = function Point_getDescription()
{
    return "point ("+this.point.lat().toFixed(5)+","+this.point.lng().toFixed(5)+")";
}

Point.prototype.dragEnd = function Point_dragEnd()
{
    // update point coordinates
    this.point = this.overlay.getPoint();
    this.setDirty();
    digitize.setDirty();
}

Point.prototype.move = function Point_move(point) {
    this.point = point;
    this.refresh();
}

Point.prototype.getBoundingBox = function Point_getBoundingBox()
{
    return new GLatLngBounds(this.point, this.point);
}

//----------------------------------------------------------------------

function Polyline (vertices, opt_color, opt_width, opt_opacity) {
    this.vertices = vertices;
    this.color = opt_color || "#888888";
    this.width = opt_width || 2;
    this.opacity = opt_opacity || 1;
}

Polyline.prototype = new GOverlay();

Polyline.prototype.initialize = function(map) {
    var svg = document.createElementNS("http://www.w3.org/2000/svg","svg");
    svg.style.position = "absolute";
    var path = document.createElementNS("http://www.w3.org/2000/svg","path");
    svg.appendChild(path);
    map.getPane(G_MAP_MARKER_PANE).appendChild(svg);

    svg.setAttribute('version','1.1');
    svg.setAttribute('overflow','visible');

    path.setAttribute('pointer-events','visibleStroke');
    path.setAttribute('stroke-linejoin','round');
    path.setAttribute('stroke-linecap','round');
    path.setAttribute('stroke',this.color);
    path.setAttribute('stroke-opacity',this.opacity);
    path.setAttribute('stroke-width',this.width+'px');
    path.setAttribute('fill','none');
    this.path = path;
    this.svg = svg;

    //path.setAttribute('onmouseover','log(\'svg handler\');');
    //path.addEventListener('click',function(){log('path click');}, false);
    //path.addEventListener('mouseover',function(){log('path mouseover');}, false);
    //path.addEventListener('onmouseover',function(){log('path onmouseover');}, false);
    //path.addEventListener('mouseout',function(){log('path mouseout');}, false);
}

// Remove the main DIV from the map pane
Polyline.prototype.remove = function() {
    //this.svg.removeEventListener("mouseover", this.callback, false);
    this.svg.parentNode.removeChild(this.svg);
    this.vertices = null;
    this.host = null;
}

Polyline.prototype.copy = function() {
    return new Polyline(this.vertices, this.color, this.width, this.opacity);
}

// Google uses M41,32 L535,26 L537,250 L73,244
// W3 says M 41 32 L 535 26 L 537 250 L 73 244
Polyline.prototype.redraw = function(force) {
    var minx,miny,maxx,maxy;
    if (!force) return;

    if (this.vertices.length>1) {
	var d = "";
	var action = "M";
	for (var i=0;i<this.vertices.length;i++) {
	    var pixel = map.fromLatLngToDivPixel(this.vertices[i]);
	    // hack
	    //pixel.x+=5;
	    //pixel.y+=5;
	    d+=action+pixel.x+","+pixel.y;
	    action = " L";

	    if (i==0) {
		minx = maxx = pixel.x;
		miny = maxy = pixel.y;
	    } else {
		if (pixel.x<minx) minx=pixel.x;
		if (pixel.x>maxx) maxx=pixel.x;
		if (pixel.y<miny) miny=pixel.y;
		if (pixel.y>maxy) maxy=pixel.y;
	    }
	}
	//log('path = '+d);
	this.path.setAttribute('d',d);
	var x0=minx-this.width;
	var y0=miny-this.width;
	var dx=maxx-x0+this.width;
	var dy=maxy-y0+this.width;
	this.svg.setAttribute('style','position:absolute;left:'+x0+'px;top:'+y0+'px;z-index:1000;');
	this.svg.setAttribute('width',dx+'px');
	this.svg.setAttribute('height',dy+'px');
	this.svg.setAttribute('viewBox', x0+' '+y0+' '+dx+' '+dy);
    }
}

//----------------------------------------------------------------------
function Line(vertices) {
    superfn(this, arguments);
    this.vertices = vertices || [];
    this.minVertices = 2;
    this.buildSegments();
    this.editMode = false;
}

Line.superclass = LayerNode;
Line.prototype = new LayerNode();
Line.prototype.className = 'line';
Line.prototype.toString = function() { return "line "+this.uid; };

Line.prototype.getBoundingBox = function Line_getBoundingBox()
{
    var lng0;
    var lat0;
    var lng1;
    var lat1;
    if (this.vertices.length>0) {
	lng0=lng1=this.vertices[0].lng();
	lat0=lat1=this.vertices[0].lat();
	for (var i=1;i<this.vertices.length;i++) {
	    var lat = this.vertices[i].lat();
	    var lng = this.vertices[i].lng();
	    if (lat<lat0) lat0=lat;
	    if (lat>lat1) lat1=lat;
	    if (lng<lng0) lng0=lng;
	    if (lng>lng1) lng1=lng;
	}
    }
    return new GLatLngBounds(new GLatLng(lat0,lng0),new GLatLng(lat1,lng1));
}

Line.prototype.getDescription = function Line_getDescription()
{
    var length;
    meters = parseInt(this.overlay.getLength());
    if (meters<1000) {
	length = meters + "m";
    } else if (meters<10000) {
	length = formatFloat(meters/1000,1)+"km";
    } else {
	length = parseInt(meters/1000)+"km";
    }
    return "line ("+this.vertices.length+" vertices, "+length+")";
}

Line.prototype.getPoint = function Line_getPoint() {
    var n = this.vertices.length;
    var midpoint;
    if (n==0) {
	midpoint = map.getCenter(); // hack
	log('midpoint is mapCenter ='+midpoint);
    } else if (n%2) {
	var m = (n-1)/2;
	// odd number of vertices - pick middle one
	midpoint = this.vertices[m];
	log('midpoint is vertex '+m+'='+this.vertices[m]);
    } else {
	var m = n/2-1;
	midpoint = new GLatLng((this.vertices[m].lat()+this.vertices[m+1].lat())/2,
			       (this.vertices[m].lng()+this.vertices[m+1].lng())/2);
	log('midpoint is midway between '+m+' and the next vertex');
    }
    log('midpoint='+midpoint.lat()+','+midpoint.lng());
    return midpoint;
}

Line.prototype.getXmlChildren = function Line_getXmlChildren()
{
    var xml = '<vertices>';
    for (var i=0;i<this.vertices.length;i++) {
	xml+='<vertex';
	xml+=' lat="'+this.vertices[i].lat()+'"';
	xml+=' lng="'+this.vertices[i].lng()+'"/>';
    }
    xml+= '</vertices>';
    return xml;
}

Line.prototype.getXml = function Line_getXml(node)
{
    if (this.vertices.length==0) {
	xml = "";
    } else {
	xml = Line.superclass.prototype.getXml.call(this,node);
    }
    return xml;
}

Line.prototype.loadXmlChildren = function Line_loadXmlChildren(node)
{
    var vertexList = node.getElementsByTagName("vertex");
    for (var i=0;i<vertexList.length;i++) {
	var v = vertexList.item(i);
	var p = new GLatLng(parseFloat(v.getAttribute("lat")),
			    parseFloat(v.getAttribute("lng")));
	this.vertices.push(p);
    }
    this.buildSegments();
}

Line.prototype.getPixelDistance = function Line_getPixelDistance(pixelPt, segment)
{
    // if zoom has changed, need to recompute
    if (this.segmentZoom != currentZoom) {
	this.buildSegments();
    }
    // reset origin in case map has scrolled (?)
    this.segments[segment].setOrigin(map.fromLatLngToDivPixel(this.vertices[segment]));
    var d = this.segments[segment].getSimpleDistance(pixelPt);
    // log("simple distance = "+d);
    return d;
}

Line.prototype.findCloser = function Line_findCloser(pixelPt, closest)
{
    var closer = false;
    // log("checking "+this.segments.length+" segments");
    for (var i=0;i<this.segments.length;i++) {
	var d = this.getPixelDistance(pixelPt, i);
	if (closest.feature==null) {
	    //log("feature "+this.uid+" seg "+i+" distance="+d);
	}
	if (d<closest.distance) {
	    closest.feature = this;
	    closest.segment = i;
	    closest.distance=d;
	    closer = true;
	    log("closest seg is "+this.uid+":"+i);
	}
    }
    return closer;
}

Line.prototype.addOverlay = function Line_addOverlay(map) {
    this.overlay = new GPolyline(this.vertices, "#0000ff", 3, 0.5);
    // Line overlay will steal mouse clicks from the map.
    // Add listener to propagate them back to the lineClick handler.
    var me = this;
    GEvent.addListener(this.overlay,'click',function(point, index){me.onClick(point,index);});
    GEvent.addListener(this.overlay,'lineupdated',function(){me.onLineUpdated(arguments);});
    GEvent.addListener(this.overlay,'mouseover',function(point){me.mouseOver(point);});
    //GEvent.addListener(this.overlay,'dblclick',function(point){alert('line dblclick');});  // ssems to do nothing
    //GEvent.addDomListener(this.overlay,'keypress',function(){alert('keypress');});
    //GEvent.addListener(this.overlay,'singlerightclick',function(){alert('rightclick');});
    map.addOverlay(this.overlay);
}

// The lineudpated event sent when a user updates the line geometry
// doesn't pass any detail, so we need to recapture the geometry from
// the overlay.  NOTE: now that we are using Google's editing code, we could
// just use the overlay to store the coordinates and do away with this.vertices.
Line.prototype.getVerticesFromOverlay = function Line_getVerticesFromOverlay()
{
    this.vertices = [];
    var n=this.overlay.getVertexCount();
    for (var i=0;i<n;i++) {
	this.vertices.push(this.overlay.getVertex(i));
    }
    this.setDirty();
    digitize.setDirty();
}

// this.segments is an array of Segment() objects
// used to accelerate testing for mouseover events.
// We don't get a native mouseover function, so 
// instead we use some matrix-algebra based code
// to determine distance in pixels between each line
// segment and the cursor.
Line.prototype.buildSegments = function()
{
  this.segmentZoom = currentZoom;  // only applicable at this zoom level
  this.segments = [];
  if (this.vertices.length>1) {
    var A = map.fromLatLngToDivPixel(this.vertices[0]);
    for (var i=1;i<this.vertices.length;i++) {
      var B = map.fromLatLngToDivPixel(this.vertices[i]);
      this.segments.push(new Segment(A,B));
      A=B;
    }
  }
}

Line.prototype.updateSegment = function Line_updateSegment(index) {
    var A = map.fromLatLngToDivPixel(this.vertices[index]);
    var B = map.fromLatLngToDivPixel(this.vertices[index+1]);
    this.segments[index].setVertices(A,B);
}

Line.prototype.insertSegment = function Line_insertSegment(index) {
    var A = map.fromLatLngToDivPixel(this.vertices[index]);
    var B = map.fromLatLngToDivPixel(this.vertices[index+1]);
    this.segments.splice(index, 0, new Segment(A,B));
}

Line.prototype.addVertex = function Line_addVertex(point)
{
    this.vertices.push(point);
    if (this.vertices.length>1) {
      this.insertSegment(this.vertices.length-2);
    }
    this.refresh();
}

Line.prototype.moveVertex = function Line_moveVertex(index, point) {
    if (index<0) {
	index = this.vertices.length+index;
    }
    if (index>=0 && index<this.vertices.length) {
      this.vertices[index]=point;
      // reset segment n-1 and n
      if (index>0) {
	  this.updateSegment(index-1);
      }
      if (index<this.segments.length) {
	  this.updateSegment(index);
      }	
    } else {
	log('vertex out of range');
    }
    this.refresh();
}

// inserts BEFORE vertex <index>
Line.prototype.insertVertex = function Line_insertVertex(index, point) {
    if (index<0) {
	index = this.vertices.length+index;
    }
    if (index>=0 && index<this.vertices.length) {
	this.vertices.splice(index, 0, point);
	this.insertSegment(index);
	if (index>0) {
	    this.updateSegment(index-1);
	}
    }
}

Line.prototype.deleteVertex = function Line_deleteVertex(index) {

    if (this.vertices.length<=2) {
	// never allow 2nd last to be deleted.
	return;
    }
    //log('deleting vertex '+index+' of '+this.vertices.length);
    this.setDirty();
    digitize.setDirty();

    if (index>=0 && index<this.vertices.length) {
	this.vertices.splice(index,1);
        this.overlay.deleteVertex(index);
    } else {
	log('vertex out of range');
    }
}

Line.prototype.onClick = function Line_onClick(latlng, index) {
    if (digitize.mapModeSelected || index==undefined) {
	digitize.lineClick(this, latlng);
    } else {
	this.deleteVertex(index);
    }
}

Line.prototype.mouseOver = function Line_mouseOver() {
    if (digitize.mapModeSelected==false) { // ie we are not drawing a new line
	this.enableEditing();
    }
}

Line.prototype.enableEditing = function Line_enableEditing()
{
    this.editMode = true;
    if (digitize.editShape) {
	digitize.editShape.disableEditing();
    }
    digitize.editShape = this;
    this.overlay.enableEditing();
}

Line.prototype.onLineUpdated = function Line_onLineUpdated(args)
{
    // copy the vertices back from the line
    this.getVerticesFromOverlay();
}    

Line.prototype.disableEditing = function Line_disableEditing()
{
    this.editMode = false;
    this.overlay.disableEditing();
}

Line.prototype.setMode = function Line_setMode(mode)
{
    if (mode=='scroll') {
	//log('overlay='+this.overlay)
	//modeListeners.push(GEvent.bindDom(this.overlay.path, 'mouseover', this, this.mouseOver));
	//modeListeners.push(GEvent.bindDom(this.overlay.path, 'mouseout', this, this.mouseOut));
    }
}

Line.prototype.getEdgePoint = function Line_getEdgePoint(edgeNo)
{
    var pt = null;
    if (edgeNo>=0 && edgeNo<(this.vertices.length-1)) {
	//log("midway ");
	var lat = (this.vertices[edgeNo].lat() + this.vertices[edgeNo+1].lat())/2.0;
	var lng = (this.vertices[edgeNo].lng() + this.vertices[edgeNo+1].lng())/2.0;
	pt = new GLatLng(lat,lng);
	//log(pt);
    }
    return pt;
}

//----------------------------------------------------------------------
function Polygon(vertices) {
    superfn(this, arguments);
    this.vertices = vertices || [];
    this.buildSegments();
    this.minVertices = 3;
}
Polygon.superclass = LayerNode;   
Polygon.prototype = new Line();
Polygon.prototype.className = 'polygon';
Polygon.prototype.addOverlay = function Polygon_addOverlay(map) {
    this.overlay = new GPolygon(this.vertices, "#ff0000", 2, 0.7, "#0000ff", 0.4);
    var me = this;
    GEvent.addListener(this.overlay,'click',function(point, index){me.onClick(point,index);});
    GEvent.addListener(this.overlay,'lineupdated',function(){me.onLineUpdated(arguments);});
    GEvent.addListener(this.overlay,'mouseover',function(point){me.mouseOver(point);});
    map.addOverlay(this.overlay);
}

Polygon.prototype.getDescription = function Polygon_getDescription()
{
    sqMeters = parseInt(this.overlay.getArea());
    if (sqMeters<10000) {
	area = sqMeters+"sq.m";
    } else if (sqMeters<100000) {
	area = formatFloat(sqMeters/10000,1)+"ha";
    } else {
	area = parseInt(sqMeters/10000)+"ha";
    }
    return "polygon ("+this.vertices.length+" vertices, "+area+")";
}

Polygon.prototype.addVertex = function Polygon_addVertex(point)
{
    if (this.vertices.length==0) {
	this.vertices.push(point); 
	this.vertices.push(point); 
	this.insertSegment(0);   
    } else {
	this.vertices[this.vertices.length-1]=point;  // replace last point with new point
	this.vertices.push(this.vertices[0]);         // then add closure
	this.updateSegment(this.vertices.length-3);   // has at least 3 pts now
	this.insertSegment(this.vertices.length-2);   
    }

    this.refresh();
}

Polygon.prototype.onClick = function Polygon_onClick(latlng, index) {
    if (digitize.mapModeSelected || index==undefined) {
	digitize.polygonClick(this, latlng);
    } else {
	this.deleteVertex(index);
    }
}

Polygon.prototype.moveVertex = function Polygon_moveVertex(index, point) {
    if (index<0) {
	index = this.vertices.length+index;
    }

    // polygon is closed: last and first vertex move together
    // last -> first
    if (index==this.vertices.length-1) {
	index=0;
    }

    if (index>=0 && index<this.vertices.length) {
      this.vertices[index]=point;
      if (index>0) {
	  // reset segment n-1 and n
	  this.updateSegment(index);
	  this.updateSegment(index-1);
      } else {
	  this.vertices[this.vertices.length-1]=point;
	  this.updateSegment(0);
	  this.updateSegment(this.vertices.length-2);
      }

    } else {
	log('vertex out of range');
    }
    this.refresh();
}

Polygon.prototype.deleteVertex = function Polygon_deleteVertex(index) {

    if (this.vertices.length<=3) {
	// never allow 3rd last to be deleted.
	return;
    }

    //log('deleting vertex '+index+' of '+this.vertices.length);
    this.setDirty();
    digitize.setDirty();

    // closed polygon - last -> first
    if (index==this.vertices.length-1) {
	index=0;
    }

    if (index>=0 && index<this.vertices.length) {
	this.vertices.splice(index,1);
        this.overlay.deleteVertex(index);
    } else {
	log('vertex out of range');
    }
}

//----------------------------------------------------------------------
function Feature()
{
    superfn(this, arguments);
    this.geometry = null;
}

Feature.superclass = LayerNode;
Feature.prototype = new LayerNode();
Feature.prototype.className = 'feature';
Feature.prototype.loadXmlChildren = function Feature_loadXmlChildren(node)
{
    if (node.childNodes.length>0) {
	var geometryNode = node.firstChild;
	this.geometry = this.createGeometry(geometryNode);
    } else {
	this.geometry = null;
    }
    this.geometry.feature = this;  // need to link back to feature to get at attributes
}

Feature.prototype.getXmlChildren = function Feature_getXmlChildren()
{
    var xml = '';
    if (this.geometry!=null) {
      xml += this.geometry.getXml();
    }
    return xml;
}

Feature.prototype.createGeometry = function Feature_createGeometry(node)
{
    var geometry;
    var geometryType = node.nodeName;
    if (geometryType=='point') {
        geometry = new Point();
    } else if (geometryType=='line') {
	geometry = new Line();
    } else if (geometryType=='polygon') {
	geometry = new Polygon();
    } else {
	alert('unknown geometry type: '+geometryType);
    }
    geometry.loadXml(node);
    return geometry;
}

Feature.prototype.show = function Feature_show()
{
    if (this.geometry!=null) {
	this.geometry.show();
    }
}

Feature.prototype.hide = function Feature_hide()
{
    if (this.geometry!=null) {
	this.geometry.hide();
    }
}

//----------------------------------------------------------------------
function SchemaField() {
    this.readOnly = false;
    this.repeat_last = 'f';  // t or f
    this.default_value = '';
    this.required = 'f';  // t or f
    this.input = null;  // input DOM object
    this.warning = null;  // img DOM object
}

SchemaField.superclass = LayerNode;
SchemaField.prototype = new LayerNode();
SchemaField.prototype.className = 'schemafield';

SchemaField.prototype.getForm = function SchemaField_getForm()
{
    this.div = document.createElement('div');
    this.div.appendChild(document.createTextNode(this.prompt+':'));
    this.div.appendChild(document.createElement('br'));
    this.input = this.createInput();
    this.div.appendChild(this.input);

    if (this.required=='t') {
      this.warning = document.createElement('img');
      this.warning.src = '/images/required.gif';
      this.warning.title = 'required';
      this.warning.className = 'warningx';
      this.div.appendChild(this.warning);
    }
    return this.div;
}

SchemaField.prototype.setValue = function SchemaField_setValue(value)
{
    this.input.value = value;  // for input
    this.input.disabled = this.readOnly;
}

SchemaField.prototype.resetValue = function SchemaField_resetValue()
{
    if (this.repeat_last=='t') {
	// do nothing
    } else {
	this.setValue(this.default_value);
    }
}

SchemaField.prototype.getValue = function SchemaField_getValue()
{
    var value = this.input.value;
    value = value.replace(/^\s+|\s+$/g, '');
    return value;
}

SchemaField.prototype.validate = function SchemaField_validate()
{
    var valid = (this.required!='t') || (this.getValue()!='');
    return valid;
}

//----------------------------------------------------------------------
function TextField() 
{
}
TextField.superclass = SchemaField;
TextField.prototype = new SchemaField();
TextField.prototype.className = 'textfield';

TextField.prototype.createInput = function TextField_createInput()
{
    var node = document.createElement('input');
    node.type = "text";
    node.size = this.length;
    return node;
}

//----------------------------------------------------------------------
function SelectField() {
    this.options = [];
}
SelectField.superclass = SchemaField;
SelectField.prototype = new SchemaField();
SelectField.prototype.className = 'selectfield';

SelectField.prototype.createInput = function SelectField_createInput()
{
    var node = document.createElement('select');
    for (var i=0;i<this.options.length;i++) {
        var option = document.createElement('option');
        option.value = this.options[i].value;
        option.appendChild(document.createTextNode(this.options[i].name));
        node.appendChild(option);
    }
    return node;
}

SelectField.prototype.loadXmlChildren = function SelectField_loadXmlChildren(node)
{
    this.options = [];
    var optionNodes = node.getElementsByTagName('option');
    for (var i=0;i<optionNodes.length;i++) {
	var optionNode = optionNodes[i];
	var name = optionNode.getAttribute('name');
	var value = optionNode.getAttribute('value');
        this.options.push({name:name, value:value});
    }
}

//----------------------------------------------------------------------
function DateField() {
    this.options = [];
}

DateField.superclass = SchemaField;
DateField.prototype = new SchemaField();
DateField.prototype.className = 'textfield';
DateField.prototype.createInput = function DateField_createInput()
{
    var node = document.createElement('input');
    node.type = "text";
    node.size = this.length;
    $(node).datepicker({dateFormat: 'dd/mm/yy', firstDay:0}); // or use firstDay:1 for MTWTFSS
    return node;
}

//----------------------------------------------------------------------
function TextareaField() {
}
TextareaField.superclass = SchemaField;
TextareaField.prototype = new SchemaField();
TextareaField.prototype.className = 'textareafield';

TextareaField.prototype.createInput = function TextareaField_createInput()
{
    var node = document.createElement('textarea');
    node.size = this.length;
    node.height = this.lines;
   
    this.textNode = document.createTextNode("");
    node.appendChild(this.textNode);
    return node;
}

//----------------------------------------------------------------------

function Schema() {
    superfn(this, arguments);
    this.fields = [];
    this.infoDiv = null;
    this.formDiv = null;
}
Schema.superclass = LayerNode;
Schema.prototype = new LayerNode();
Schema.prototype.className = 'schema';

Schema.prototype.loadXmlChildren = function Schema_loadXmlChildren(node)
{
     this.fields = [];
     var fieldNodes = node.getElementsByTagName('field');
     for (var i=0;i<fieldNodes.length;i++) {
        var fieldNode = fieldNodes[i];
        var type = fieldNode.getAttribute('type'); 
        var field;
        if (type=='text') {
            field = new TextField();
        } else if (type=='textarea') {
            field = new TextareaField();
        } else if (type=='select') {
            field = new SelectField();
        } else if (type=='date') {
	    field = new DateField();
	}
        field.loadXml(fieldNode);
        this.fields.push(field);
    }
}

Schema.prototype.getForm = function Schema_getForm(options)
{
    // if no options set, use these defaults
    options = options || {deleteButton: false};

    var windowDiv = document.createElement('div');
    windowDiv.className = "xmldialog";

    var titleDiv = document.createElement('div');
    titleDiv.className = "title";
    titleDiv.appendChild(document.createTextNode(this.description));
    windowDiv.appendChild(titleDiv);

    var div = document.createElement('div');
    div.className = 'desc';

    // create a text node for describing geometry
    this.descNode = document.createTextNode('');
    div.appendChild(this.descNode);

    var deleteButton = document.createElement('button');
    deleteButton.className = "delete";
    deleteButton.name = "delete";
    deleteButton.appendChild(document.createTextNode('Delete'));
    deleteButton.onclick = function() {me.onDelete();};
    div.appendChild(deleteButton);
    windowDiv.appendChild(div);
    this.deleteButton = deleteButton;

    var formDiv = document.createElement('div');
    formDiv.className = "form";
    windowDiv.appendChild(formDiv);
    this.formDiv = formDiv;  // save for extracting data
  
    for (var i=0;i<this.fields.length;i++) { 
	formDiv.appendChild(this.fields[i].getForm());
    }

    var infoDiv = document.createElement('div');
    infoDiv.className = 'info';
    infoDiv.appendChild(document.createTextNode('Please complete all required fields'));
    windowDiv.appendChild(infoDiv);
    this.infoDiv = infoDiv;

    var controlDiv = document.createElement('div');
    controlDiv.className = 'control';
    windowDiv.appendChild(controlDiv);

    var me = this;
    var saveButton = controlDiv.appendChild(document.createElement('button'));
    saveButton.className = "save";
    saveButton.name = "save";
    saveButton.appendChild(document.createTextNode('Save'));
    saveButton.onclick = function() {me.onSave();};
    controlDiv.appendChild(saveButton);

    var cancelButton = controlDiv.appendChild(document.createElement('button'));
    cancelButton.className = "cancel";
    cancelButton.name = "cancel";
    cancelButton.appendChild(document.createTextNode('Cancel'));
    cancelButton.onclick = function() {me.onCancel();};
    controlDiv.appendChild(cancelButton);

    return windowDiv;
}

Schema.prototype.showDeleteButton =function Schema_showDeleteButton(show)
{
    this.deleteButton.style.display = show ? "block" : "none";
}

Schema.prototype.onCancel = function Schema_onCancel()
{
	alert('no cancel handler');
}

Schema.prototype.onSave = function Schema_onSave()
{
	alert('no save handler');
}

Schema.prototype.onDelete = function Schema_onDelete()
{
	alert('no delete handler');
}

Schema.prototype.setValue = function Schema_setValue(object)
{
    var desc = "";
    if (typeof(object.geometry)=='object')
    {
	// eg "point (lat,lng)
	desc = object.geometry.getDescription();
    }
    this.descNode.nodeValue = desc;  // set text node to description
    

    for (var i=0;i<this.fields.length;i++) {
	var field = this.fields[i];
	var name = field.name;
	var value = object[name];
	field.setValue(value==null ? '' : value);
    }
}

Schema.prototype.resetValue = function Schema_resetValue()
{
    for (var i=0;i<this.fields.length;i++) {
	var field = this.fields[i];
	field.resetValue();
    }
}

Schema.prototype.getValue = function Schema_getValue(object)
{
    for (var i=0;i<this.fields.length;i++) {
	var field = this.fields[i];
	var name = field.name;
	var value = field.getValue();
	object[name] = value==null ? '' : value;
	object.attrib[name]=null;  // indicate the attribute is to be included in XML output 
    }
}

Schema.prototype.setReadOnly = function Schema_setReadOnly(fieldName, readOnly)
{
    for (var i=0;i<this.fields.length;i++) {
	if (this.fields[i].name==fieldName) {
	    this.fields[i].readOnly = readOnly;
	    break;
	}
    }
}

Schema.prototype.getFormXml = function Schema_getFormXml()
{
    var xml = '<record>';
    for (var i=0;i<this.fields.length;i++) {
	var field = this.fields[i];
	var name = field.name;
	var value = field.getValue();
	xml+='<field name="'+field.name+'" value="'+xmlEntities(value)+'"/>"';
    }
    xml+= '</record>';
    log(xml);
    return xml;
}

Schema.prototype.validate = function Schema_validate()
{
    var valid = true;
    for (var i=0;i<this.fields.length;i++) {
	var field = this.fields[i];
	valid &= field.validate();
    }
    this.infoDiv.style.display = valid ? 'none' : 'block';
    return valid;
}

//----------------------------------------------------------------------
function Layer() {
    superfn(this, arguments);
    this.schema = null;
    this.features = [];
}

Layer.superclass = LayerNode;
Layer.prototype = new LayerNode();
Layer.prototype.className = 'layer';

Layer.prototype.loadXmlChildren = function Layer_loadXmlChildren(node)
{
    // expect 0 or 1 schema nodes
    var schemaNodes = node.getElementsByTagName('schema');
    if (schemaNodes.length>0) {
	this.schema = new Schema();
	this.schema.loadXml(schemaNodes[0]);
    } else {
	this.schema = null;
    }

    // expect any number of feature nodes
    var featureList = node.getElementsByTagName("feature");
    for (var i=0;i<featureList.length;i++) {
      var featureNode = featureList[i];
      var feature = new Feature();
      feature.loadXml(featureNode);
      this.features.push(feature);
    }
}

Layer.prototype.getXmlChildren = function Layer_getXmlChildren()
{
    var xml = '';
    if (this.schema!=null) {
        xml+= this.schema.getXml();
    }
    xml += "<features>";
    for (var i=0;i<this.features.length;i++) {
	xml+= this.features[i].getXml();
    }
    xml += "</features>";
    return xml;
}

Layer.prototype.show = function Layer_show()
{
    for (var i=0;i<this.features.length;i++) 
    {
  	this.features[i].show();
    }
}


Layer.prototype.hide = function Layer_hide()
{
    for (var i=0;i<this.features.length;i++) 
    {
  	this.features[i].hide();
    }
}


Layer.prototype.deleteFeature = function Layer_deleteFeature(feature)
{
    for (var i=0;i<this.features.length;i++) {
	if (this.features[i] == feature) {
	    this.features.splice(i,1);
	    break;  // only return one
	}
    }
}

