// Base Shape
function PidfShape()
{
    this.type = '<Generic Shape>';
    return this;
}

PidfShape.NS_GML = 'http://www.opengis.net/gml';
PidfShape.NS_GEOSHAPE = 'http://www.opengis.net/pidflo/1.0';
PidfShape.URN_RADIANS = 'urn:ogc:def:uom:EPSG::9101';
PidfShape.URN_WGS84_3D = 'urn:ogc:def:crs:EPSG::4979';


PidfShape.prototype.load = function(xml) {};

PidfShape.prototype.getCentroid = function()
{
    return null;
};

PidfShape.prototype.getUncertainty = function()
{
    return 0;
};

PidfShape.prototype.toString = function()
{
    return this.type;
};


PidfShape.prototype.fuzz = function(size, seed)
{
    if (size <= 0)
    {
        return this;
    }
    if (this.getUncertainty() > size)
    {
        return new PidfCircle(this.getCentroid(), this.getUncertainty());
    }
    var moved = LocMath.fuzzPoint(this.getCentroid(), size - this.getUncertainty(), seed);
    return new PidfCircle(moved, size);
};

PidfShape.prototype.getNumbers = function(xml)
{
    var txt = xml.textContent ? xml.textContent : xml.text;
    txt = txt.trim().replace(/\s+/g, ' ');
    var numbers = txt.split(' ');
    return numbers.map(parseFloat);
};

PidfShape.prototype.toGMapShape = function()
{
    throw new Error('Not implemented');
};

// Point
function PidfPoint(pt)
{
    this.type = 'Point';
    this.point = pt;
    return this;
}

PidfPoint.prototype = new PidfShape();

PidfPoint.prototype.getCentroid = function()
{
    return this.point;
};

PidfPoint.prototype.load = function(xml)
{
    var posnode = findFirstElement(xml, PidfShape.NS_GML, 'pos', 'gml');
    var num = this.getNumbers(posnode);
    this.point = new GeoPoint(num[0], num[1], num[2]);
};

PidfPoint.prototype.toString = function()
{
    return PidfShape.prototype.toString.apply(this, []) + ': ' + this.point.toString();
};

if (GMapPoint)
{
    PidfPoint.prototype.toGMapShape = function()
    {
        return new GMapPoint(this.point.toGLatLng());
    };
}

// Circle
function PidfCircle(pt, radius)
{
    this.type = 'Circle';
    PidfPoint.apply(this, [pt]);
    this.uncertainty = radius;
    return this;
}

PidfCircle.prototype = new PidfPoint();

PidfCircle.prototype.load = function(xml)
{
    PidfPoint.prototype.load.apply(this, [xml]);

    var rnode = findFirstElement(xml, PidfShape.NS_GEOSHAPE, 'radius', 'gs');
    this.uncertainty = this.getNumbers(rnode)[0];
};

PidfCircle.prototype.toString = function()
{
    var txt = PidfPoint.prototype.toString.apply(this, []);
    txt += ' +/- ' + this.uncertainty + ' metres';
    return txt;
};

PidfCircle.prototype.getUncertainty = function()
{
    return this.uncertainty;
};

if (GMapEllipse)
{
    PidfCircle.prototype.toGMapShape = function()
    {
        return new GMapEllipse(this.point.toGLatLng(), this.uncertainty);
    };
}

// Sphere
function PidfSphere()
{
    this.type = 'Sphere';
    return this;
}

PidfSphere.prototype = new PidfCircle();

// Ellipse
function PidfEllipse()
{
    this.type = 'Ellipse';
    return this;
}

PidfEllipse.prototype = new PidfPoint();

PidfEllipse.prototype.load = function(xml)
{
    PidfPoint.prototype.load.apply(this, [xml]);

    var node = findFirstElement(xml, PidfShape.NS_GEOSHAPE, 'semiMajorAxis', 'gs');
    this.semiMajor = this.getNumbers(node)[0];
    node = findFirstElement(xml, PidfShape.NS_GEOSHAPE, 'semiMinorAxis', 'gs');
    this.semiMinor = this.getNumbers(node)[0];
    node = findFirstElement(xml, PidfShape.NS_GEOSHAPE, 'orientation', 'gs');
    this.orientation = this.getNumbers(node)[0];
    if (node.getAttribute('uom') == PidfShape.URN_RADIANS)
    {
        this.orientation *= 180 / Math.PI;
    }
};

PidfEllipse.prototype.toString = function()
{
    var txt = PidfPoint.prototype.toString.apply(this, []);
    txt += ' +/- semi-major ' + this.semiMajor + ' metres; semi-minor '
        + this.semiMinor + ' metres; orientation ' + this.orientation + ' degrees';
    return txt;
};

PidfEllipse.prototype.getUncertainty = function()
{
    return this.semiMajor;
};

if (GMapEllipse)
{
    PidfEllipse.prototype.toGMapShape = function()
    {
        return new GMapEllipse(this.point.toGLatLng(), this.semiMajor, this.semiMinor, this.orientation);
    };
}

// Ellipsoid
function PidfEllipsoid()
{
    this.type = 'Ellipsoid';
    return this;
}

PidfEllipsoid.prototype = new PidfEllipse();

PidfEllipsoid.prototype.load = function(xml)
{
    PidfEllipse.prototype.load.apply(this, [xml]);

    var node = findFirstElement(xml, PidfShape.NS_GEOSHAPE, 'verticalAxis', 'gs');
    this.vertical = this.getNumbers(node)[0];
};

PidfEllipsoid.prototype.toString = function()
{
    var txt = PidfEllipse.prototype.toString.apply(this, []);
    txt += ', vertical ' + this.vertical + ' metres';
    return txt;
};

PidfEllipsoid.prototype.getUncertainty = function()
{
    return Math.max(this.semiMajor, this.vertical);
};

// Polygon
function PidfPolygon()
{
    this.type = 'Polygon';
    return this;
}

PidfPolygon.prototype = new PidfShape();

PidfPolygon.prototype.load = function(poly)
{
    this.points = [];
    this.is3d = (poly.getAttribute('srsName') == PidfShape.URN_WGS84_3D);
    var ring = findFirstElement(poly, PidfShape.NS_GML, 'LinearRing', 'gml');
    for (var node = ring.firstChild; node; node = node.nextSibling)
    {
        if (node.nodeType == 1) // Node.ELEMENT_NODE
        {
            xy = this.getNumbers(node);
            var local = node.localName ? node.localName : node.baseName;
            if (local == 'pos' || local == 'posList')
            {
                while (xy.length >= (this.is3d ? 3 : 2))
                {
                    var pt = new GeoPoint(xy.shift(),
                                          xy.shift(),
                                          (this.is3d ? xy.shift() : 0));
                    this.points.push(pt);
                }
            }
        }
    }
    if (this.points.length < 4)
    {
        throw "Not enough points for a polygon: " + this.points.length;
    }
    this.points.pop();  // remove duplicated point
};

PidfPolygon.prototype.calculateCentroid = function()
{
    // Convery to ECEF
    var ecef = this.points.map(function(x) {
        return x.toEcefPoint();
    });
    // Find the up normal
    var upnormal = LocMath.polygonUpNormal(ecef);
    // Find a transformation matrix to neutralize x
    var t = LocMath.planeOrient(upnormal);
    // Apply transform
    var transformed = ecef.map(function(e) {
        return LocMath.coordinateTransform(t, e.toArray(), false);
    });
    // Find a centroid on the transformed x-y plane
    var trans_centroid = LocMath.xyPolygonCentroid(transformed);
    // Reverse transform
    var ecef_centroid = LocMath.coordinateTransform(t, trans_centroid, true);
    // Convert back to Geodetic
    ecef_centroid = new EcefPoint(ecef_centroid);
    this.centroid = ecef_centroid.toGeoPoint();
    this.centroid.altitude = this.getCentroidAltitude();

    // Find furthest point for uncertainty
    this.uncertainty = 0;
    for (var i in ecef)
    {
        i = parseInt(i); if (isNaN(i)) { continue; }

        var dist = ecef[i].distanceTo(ecef_centroid);
        if (dist > this.uncertainty)
        {
            this.uncertainty = dist;
        }
    }

    this.upNormalIsUp = (ecef_centroid.x * upnormal[0] +
                         ecef_centroid.y * upnormal[1] +
                         ecef_centroid.z * upnormal[2]) > 0;
};

PidfPolygon.prototype.getCentroidAltitude = function()
{
    var alt = this.points[0].altitude;
    for (i in this.points)
    {
        i = parseInt(i); if (isNaN(i)) { continue; }

        if (alt != this.points[i].altitude)
        {
            return this.centroid.altitude;
        }
    }
    return alt;
};

PidfPolygon.prototype.getCentroid = function()
{
    if (! this.centroid)
    {
        this.calculateCentroid();
    }
    return this.centroid;
};

PidfPolygon.prototype.isUpNormalUp = function()
{
    if (! this.centroid)
    {
        this.calculateCentroid();
    }
    return this.upNormalIsUp;
};

PidfPolygon.prototype.getUncertainty = function()
{
    if (! this.uncertainty)
    {
        this.calculateCentroid();
    }
    return this.uncertainty;
};

PidfPolygon.prototype.toString = function()
{
    var txt = PidfShape.prototype.toString.apply(this, []);
    if (! this.isUpNormalUp())
    {
        txt += ' <clockwise>';
    }
    txt += ':';
    for (var i in this.points)
    {
        i = parseInt(i); if (isNaN(i)) { continue; }

        txt += (i > 0 ? ',' : '');
        txt += ' [' + i + '] ' + this.points[i].toString();
    }
    return txt;
};

if (GMapPolygon)
{
    PidfPolygon.prototype.toGMapShape = function()
    {
        var tll = function(pt) { return pt.toGLatLng() };
        return new GMapPolygon(this.points.map(tll),
                               this.getCentroid().toGLatLng(),
                               this.getUncertainty());
    };
}

// Prism
function PidfPrism()
{
    this.type = 'Prism';
    return this;
}

PidfPrism.prototype = new PidfPolygon();

PidfPrism.prototype.load = function(xml)
{
    PidfPolygon.prototype.load.apply(this, [xml]);

    var node = findFirstElement(xml, PidfShape.NS_GEOSHAPE, 'height', 'gs');
    this.height = this.getNumbers(node)[0];
};

PidfPrism.prototype.toString = function()
{
    var txt = PidfPolygon.prototype.toString.apply(this, []);
    txt += ', height ' + this.height + ' metres';
    return txt;
};

PidfPrism.prototype.getCentroidAltitude = function()
{
    var alt = PidfPolygon.prototype.getCentroidAltitude.apply(this, []);
    alt += this.height / 2;
    return alt;
};

// ArcBand
function PidfArcBand()
{
    this.type = 'ArcBand';
    return this;
}

PidfArcBand.prototype = new PidfPoint();

PidfArcBand.prototype.load = function(xml)
{
    PidfPoint.prototype.load.apply(this, [xml]);

    var node = findFirstElement(xml, PidfShape.NS_GEOSHAPE, 'innerRadius', 'gs');
    this.innerRadius = this.getNumbers(node)[0];
    node = findFirstElement(xml, PidfShape.NS_GEOSHAPE, 'outerRadius', 'gs');
    this.outerRadius = this.getNumbers(node)[0];
    node = findFirstElement(xml, PidfShape.NS_GEOSHAPE, 'startAngle', 'gs');
    this.startAngle = this.getNumbers(node)[0];
    if (node.getAttribute('uom') == PidfShape.URN_RADIANS)
    {
        this.startAngle *= 180 / Math.PI;
    }
    node = findFirstElement(xml, PidfShape.NS_GEOSHAPE, 'openingAngle', 'gs');
    this.openingAngle = this.getNumbers(node)[0];
    if (node.getAttribute('uom') == PidfShape.URN_RADIANS)
    {
        this.openingAngle *= 180 / Math.PI;
    }
};

PidfArcBand.prototype.toString = function()
{
    var txt = PidfPoint.prototype.toString.apply(this, []);
    txt += ', inner radius ' + this.innerRadius + ' metres';
    txt += ', outer radius ' + this.outerRadius + ' metres';
    txt += ', start angle ' + this.startAngle + ' degrees';
    txt += ', opening angle ' + this.openingAngle + ' degrees';
    return txt;
};

PidfArcBand.prototype.calculateCentroid = function()
{
    var halfOpening = this.openingAngle * Math.PI / 360;
    var d = 4 * Math.sin(halfOpening)
        * (this.outerRadius * this.outerRadius
           + this.outerRadius * this.innerRadius
           + this.innerRadius * this.innerRadius)
        / (6 * halfOpening * (this.outerRadius + this.innerRadius));

    var startRadians = this.startAngle * Math.PI / 180;
    var angle = startRadians + halfOpening;

    this.centroid = LocMath.movePoint(this.point, d, angle);
    this.centroid.altitude = null;

    var coso2 = Math.cos(halfOpening);
    var toOuter = Math.sqrt(d * d + this.outerRadius * this.outerRadius - 2 * d * this.outerRadius * coso2);
    var toInner = Math.sqrt(d * d + this.innerRadius * this.innerRadius - 2 * d * this.innerRadius * coso2);
    this.uncertainty = Math.max(toInner, toOuter);
};

PidfArcBand.prototype.getCentroid = function()
{
    if (! this.centroid)
    {
        this.calculateCentroid();
    }
    return this.centroid;
};

PidfArcBand.prototype.getUncertainty = function()
{
    if (! this.uncertainty)
    {
        this.calculateCentroid();
    }
    return this.uncertainty;
};

if (GMapArcBand)
{
    PidfArcBand.prototype.toGMapShape = function()
    {
        return new GMapArcBand(this.point.toGLatLng(),
			       this.innerRadius, this.outerRadius,
			       this.startAngle, this.openingAngle,
                               this.getCentroid().toGLatLng(),
                               this.getUncertainty());
    };
}
