// If you know the URL of a LIS, you can make a very simple HELD request to
// retrieve a PIDF-LO.
//   var held = new HeldLocationRequest('http://lis.example.org/');
//
// An asynchronous version uses additional callback methods:
//   var held = new HeldLocationRequest('http://lis.example.org/',
//                function(loc) { alert('Location: ' + loc); },
//                function(code,txt) { alert('Error: ' + code); });
//
// A null callback (second parameter) ensures that a synchronous request
// is made.
//
// An IPv4 address for the target can be specified:
//   held.ip = '1.2.3.4';
// A null IP (the default) uses the IP address of the browser host as seen
// by the LIS.
//
// Location types can be specified.
//   held.types = ['geodetic'];
// The 'types' member is an array of strings: 'geodetic', 'civic'.
//
// The request is sent using .send(), which is synchronous and returns a
// location if the request was created without a callback:
//   var location = held.send();
//
// Note: security restrictions on XMLHttpRequest mean that this should be
// the same server as the included page.
function HeldLocationRequest(_url, _callback, _errorCallback)
{
    this.url = _url;
    this.callback = _callback;
    this.errorCallback = _errorCallback;
}

HeldLocationRequest.prototype =
{
    request: getXMLHttpRequest(),
    ip: null,
    types: [],

    constructQuery: function()
    {
        var query = '<locationRequest xmlns="' + PidfLocation.NS_HELD + '">';
        if (this.types.length > 0)
        {
            query += '<locationType>' + this.types.join(' ') + '</locationType>';
        }

        if (this.ip)
        {
            query += '<deviceIdentity xmlns="' + PidfLocation.NS_HELD + ':id">';
            query += '<uri>ip:IPv4+' + this.ip + '</uri>';
            query += '</deviceIdentity>';
        }
        query += '</locationRequest>';
        return query;
    },

    ready: function()
    {
        if (this.request.readyState == 4)
        {
             // status of 0 appears when working from local files
            if ((this.request.status == 200 || this.request.status == 0)
                    && this.request.responseXML.documentElement)
            {
                return this.process(this.request.responseXML);
            }
            else if (this.errorCallback)
            {
                this.errorCallback(this.request.status,
                                   this.request.statusText);
            }
        }
    },

    process: function(xml)
    {
        var doc = xml.documentElement;
        var local = doc.localName ? doc.localName : doc.baseName;
        if (local == 'error')
        {
            if (this.errorCallback)
            {
                this.errorCallback(doc.getAttribute('code'),
                                   doc.getAttribute('message'));
            }
        }
        else if (local == 'locationResponse')
        {
            try
            {
                var loc = new PidfLocation(doc);
                if (this.callback)
                {
                    this.callback(loc);
                }
                return loc;
            }
            catch (error)
            {
                this.errorCallback('clientError', error);
            }
        }
    },

    send: function()
    {
        this.request.open("POST", this.url, this.callback != null);
        this.request.setRequestHeader('Content-Type', 'application/held+xml');
        if (this.callback)
        {
            var here = this;
            this.request.onreadystatechange = function(e) { here.ready() };
        }
        this.request.send(this.constructQuery());
        return this.ready();
    }
}

// A Geodetic object contains one of the many shapes from the PIDF-LO
// profile draft.  this.shape contains the shape.
// If you prefer it simple, try getCentroid() to get a point (which might
// have altitude).  getUncertainty() gives the uncertainty from the
// centroid.
function Geodetic(locinfo)
{
    var point = findFirstElement(locinfo, PidfShape.NS_GML, 'Point', 'gml');
    if (point)
    {
        this.shape = new PidfPoint();
        this.shape.load(point);
    }
    var prism = findFirstElement(locinfo, PidfShape.NS_GEOSHAPE, 'Prism', 'gs');
    if (prism)
    {
        this.shape = new PidfPrism();
        this.shape.load(prism);
    }
    else // Polygon is a sub-element of prism, so care needs to be taken
    {
        var polygon = findFirstElement(locinfo, PidfShape.NS_GML, 'Polygon', 'gml');
        if (polygon)
        {
            this.shape = new PidfPolygon();
            this.shape.load(polygon);
        }
    }
    var circle = findFirstElement(locinfo, PidfShape.NS_GEOSHAPE, 'Circle', 'gs');
    if (circle)
    {
        this.shape = new PidfCircle();
        this.shape.load(circle);
    }
    var sphere = findFirstElement(locinfo, PidfShape.NS_GEOSHAPE, 'Sphere', 'gs');
    if (sphere)
    {
        this.shape = new PidfSphere();
        this.shape.load(sphere);
    }
    var ellipse = findFirstElement(locinfo, PidfShape.NS_GEOSHAPE, 'Ellipse', 'gs');
    if (ellipse)
    {
        this.shape = new PidfEllipse();
        this.shape.load(ellipse);
    }
    var ellipsoid = findFirstElement(locinfo, PidfShape.NS_GEOSHAPE, 'Ellipsoid', 'gs');
    if (ellipsoid)
    {
        this.shape = new PidfEllipsoid();
        this.shape.load(ellipsoid);
    }
    var arcband = findFirstElement(locinfo, PidfShape.NS_GEOSHAPE, 'ArcBand', 'gs');
    if (arcband)
    {
        this.shape = new PidfArcBand();
        this.shape.load(arcband);
    }
    if (! this.shape)
    {
        throw 'No geodetic shape found.';
    }

    return this;
}

Geodetic.prototype = {
    getCentroid: function()
    {
        return this.shape.getCentroid();
    },

    getUncertainty: function()
    {
        return this.shape.getUncertainty();
    },

    toString: function()
    {
        return this.shape.toString()
            + '; centroid = ' + this.getCentroid()
            + '; uncertainty = ' + this.getUncertainty() + ' metres';
    }
}

// A Civic object contains all the fields derived from the civic form as
// member variables.  The format() method creates an address string based
// on a (large) number of assumptions about how the address is made.
function Civic(civicnode)
{
    for (var x = civicnode.firstChild; x; x = x.nextSibling)
    {
        if (x.nodeType == 1) // Node.ELEMENT_NODE
        {
            var txt = x.textContent ? x.textContent : x.text;
            this[x.localName ? x.localName : x.baseName] = txt.normalize();
        }
    }

    return this;

}

Civic.countryUrl = 'countries/country.php';  // where to resolve country codes
Civic.countries = [];  // a map of country codes to country names

Civic.prototype = {

    // Find the name of a country based on its code.  This requires a link to a
    // specific server-side script that returns a string based on a code
    // parameter.  See countries folder for a PHP example.
    getCountry: function(code)
    {
        if (Civic.countries[code])
        {
            return Civic.countries[code];
        }
        var cstr = code;
        try
        {
            var req = getXMLHttpRequest();
            req.open('GET', this.countryUrl + '?code=' + code, false);
            req.send(null);
            if (req.status == 200)
            {
                cstr = req.responseText;
            }
        }
        catch (err) {} // ignore
        Civic.countries[code] = cstr;
        return cstr;
    },

    // format the civic address for display.  The optional gc argument
    // should be set to true if this address is for geocoder input.
    toString: function(gc)
    {
        return ((this.NAM && !gc) ? ('"' + this.NAM + '", ') : '') +
        ((this.LOC && !gc) ? ('"' + this.LOC + '", ') : '') +
        ((this.SEAT && !gc) ? ('Seat ' + this.SEAT + ', ') : '') +
        ((this.RM && !gc) ? ('Room ' + this.RM + ', ') : '') +
        ((this.FLR && !gc) ? ('Floor ' + this.FLR + ', ') : '') +
        (this.UNIT ? ('Unit ' + this.UNIT + ', ') : '') +
        (this.BLD ? ('Building ' + this.BLD + ', ') : '') +
        (this.RD
         ? (
             (this.HNO ? (this.HNO + ' ') : '') +
             (this.PRD ? (this.PRD + ' ') : '') +
             (this.PRM ? (this.PRM + ' ') : '') +
             this.RD +
             (this.STS ? (' ' + this.STS) : '') +
             (this.POD ? (' ' + this.POD) : '') +
             (this.POM ? (' ' + this.POM) : '') +
             ((this.RDSEC && !gc) ? (' section ' + this.RDSEC) : '') +
             ((this.RDBR && !gc) ? (' and ' + this.RDBR) : '') +
             ((this.RDSUBBR && !gc) ? (' and ' + this.RDSUBBR) : '') +
             ', '
             ) : '') +
        (this.A4 ? (this.A4 + ', ') : '') +
        ((this.A3 && ! this.A4) ? (this.A3 + ', ') : '') +
        (this.A1 ? (this.A1 + ', ') : '') +
        (this.PC ? (this.PC + ', ') : '') +
        (this.ADDCODE ? (this.ADDCODE + ', ') : '') +
        this.getCountry(this.country);
    }
}

function UsageRules(xml)
{
    // Set some sensible defaults
    this.retransmission_allowed = false;
    this.retention_expires = new Date(new Date().getTime() + 24*60*60*1000);

    for (var node = xml.firstChild; node; node = node.nextSibling)
    {
        if (node.nodeType == 1) // Node.ELEMENT_NODE
        {
            var txt = node.textContent ? node.textContent : node.text;
            txt = txt.normalize();
            var local = node.localName ? node.localName : node.baseName;
            if (local == 'retransmission-allowed')
            {
                this.retransmission_allowed = (txt == 'yes') || (txt == 'true') || (txt == '1');
            }
            else if (local == 'retention-expires')
            {
                this.retention_expires.setISO8601(txt);
            }
            else if (local == 'ruleset-reference')
            {
                this.ruleset_reference = txt;
            }
            else if (local == 'note-well')
            {
                this.note_well = txt;
            }
        }
    }
    return this;
}

UsageRules.prototype.toString = function()
{
    return 'Retransmission: ' + (this.retransmission_allowed ? 'yes' : 'no')
    + ', Retention: ' + this.retention_expires
    + (this.ruleset_reference ? (', Ruleset: ' + this.ruleset_reference) : '')
    + (this.note_well ? (', Note: ' + this.note_well) : '');
};

// Make one of these, pass it an XML document that contains a PIDF-LO and it
// will attempt to extract useful information:
//   var location = new PidfLocation(req.responseXML);

// Two sub-objects are included, if the information is present in the
// PIDF-LO: location.geodetic (Geodetic) and location.civic (Civic).  See
// above for details on each of these objects.
function PidfLocation(doc)
{
    this.pidf = findFirstElement(doc, PidfLocation.NS_PIDF, 'presence');
    if (this.pidf != null)
    {
        this.entity = this.pidf.getAttribute('entity');

        try
        {
            this.geodetic = new Geodetic(this.pidf);
        }
        catch (msg)
        {
            this.geodetic = null;
        }

        var civ = findFirstElement(this.pidf, PidfLocation.NS_CIVIC, 'civicAddress', 'ca');
        if (civ)
        {
            this.civic = new Civic(civ);
        }
        else
        {
            this.civic = null;
        }
        var meth = findFirstElement(this.pidf, PidfLocation.NS_GEOPRIV, 'method', 'gp');
        if (meth)
        {
            this.method = meth.textContent ? meth.textContent : meth.text;
        }

        var ur = findFirstElement(this.pidf, PidfLocation.NS_GEOPRIV, 'usage-rules', 'gp');
        if (ur)
        {
            this.rules = new UsageRules(ur);
        }

        var ts = findFirstElement(this.pidf, PidfLocation.NS_PIDF, 'timestamp', '');
        if (! ts)
        {
            ts = findFirstElement(this.pidf, PidfLocation.NS_PIDF_DM, 'timestamp', 'dm');
        }
        if (ts)
        {
            this.timestamp = new Date();
            this.timestamp.setISO8601(ts.textContent ? ts.textContent : ts.text);
        }
    }

    var uris = findElements(doc, PidfLocation.NS_HELD, 'locationURI', 'held');
    this.locationURIs = [];
    for (var i = 0; i < uris.length; ++i)
    {
       this.locationURIs.push(uris[i].textContent ? uris[i].textContent : uris[i].text);
    }

    return this;
}

PidfLocation.NS_PIDF = 'urn:ietf:params:xml:ns:pidf';
PidfLocation.NS_PIDF_DM = PidfLocation.NS_PIDF + ':data-model';
PidfLocation.NS_GEOPRIV = PidfLocation.NS_PIDF + ':geopriv10';
PidfLocation.NS_GEOPRIV_BP = PidfLocation.NS_GEOPRIV + ':basicPolicy';
PidfLocation.NS_CIVIC = PidfLocation.NS_GEOPRIV + ':civicAddr';
PidfLocation.NS_HELD = 'urn:ietf:params:xml:ns:geopriv:held';

PidfLocation.prototype = {
    toString: function()
    {
        var txt = '';
        if (this.geodetic)
        {
            txt += "\n" + 'Geodetic: ' + this.geodetic.toString();
        }
        if (this.civic)
        {
            txt += "\n" + 'Civic: ' + this.civic.toString();
        }
        if (this.rules)
        {
            txt += "\n" + 'Usage Rules: ' + this.rules.toString();
        }
        if (this.locationURIs)
        {
            txt += "\n" + 'Location URIs: ' + this.locationURIs.join(', ');
        }
        return txt;
    }
}
