(function() {

    // fixme: make all this work with different types of "points"
    // i.e. {lat, lng}, [lat, lng]

    function rad(point) {
        return {
            lat: Ox.rad(point.lat),
            lng: Ox.rad(point.lng)
        };
    }

    /*@
    Ox.crossesDateline <f> Returns true if a given rectangle crosses the dateline
    @*/
    Ox.crossesDateline = function(point0, point1) {
        return point0.lng > point1.lng;
    }

    /*@
    Ox.getArea <f> Returns the area in square meters of a given rectancle
    @*/
    Ox.getArea = function(point0, point1) {
        /*
            area of a ring between two latitudes:
            2 * PI * r^2 * abs(sin(lat0) - sin(lat1))
            see http://mathforum.org/library/drmath/view/63767.html
        */
        /*
        2 * Math.PI *
        Math.pow(Ox.EARTH_RADIUS, 2) *
        Math.abs(Math.sin(Ox.rad(0)) - Math.sin(Ox.rad(1))) *
        Math.abs(Ox.rad(0) - Ox.rad(1)) /
        (2 * Math.PI)
        */
        if (Ox.crossesDateline(point0, point1)) {
            point1.lng += 360;
        }
        var point0 = rad(point0),
            point1 = rad(point1);
        return Math.pow(Ox.EARTH_RADIUS, 2) *
            Math.abs(Math.sin(point0.lat) - Math.sin(point1.lat)) *
            Math.abs(point0.lng - point1.lng);
    };

    /*@
    Ox.getBearing <f> Returns the bearing from one point to another
        > Ox.getBearing({lat: -45, lng: 0}, {lat: 45, lng: 0})
        0
    @*/
    Ox.getBearing = function(point0, point1) {
        var point0 = rad(point0),
            point1 = rad(point1),
            x = Math.cos(point0.lat) * Math.sin(point1.lat) -
                Math.sin(point0.lat) * Math.cos(point1.lat) *
                Math.cos(point1.lng - point0.lng),
            y = Math.sin(point1.lng - point0.lng) *
                Math.cos(point1.lat);
        return (Ox.deg(Math.atan2(y, x)) + 360) % 360;
    };

    /*@
    Ox.getCenter <f> Returns the center of a recangle on a spehre
        > Ox.getCenter({lat: -45, lng: -90}, {lat: 45, lng: 90})
        {lat: 0, lng: 0}
    @*/
    Ox.getCenter = function(point0, point1) {
        var point0 = rad(point0),
            point1 = rad(point1),
            x = Math.cos(point1.lat) *
                Math.cos(point1.lng - point0.lng),
            y = Math.cos(point1.lat) *
                Math.sin(point1.lng - point0.lng),
            d = Math.sqrt(
                Math.pow(Math.cos(point0.lat) + x, 2) + Math.pow(y, 2)
            ),
            lat = Ox.deg(
                Math.atan2(Math.sin(point0.lat) + Math.sin(point1.lat), d)
            ),
            lng = Ox.deg(
                point0.lng + Math.atan2(y, Math.cos(point0.lat) + x)
            );
        return {lat: lat, lng: lng};
    };

    /*@
    Ox.getDegreesPerMeter <f> Returns degrees per meter at a given latitude
        > 360 / Ox.getDegreesPerMeter(0)
        Ox.EARTH_CIRCUMFERENCE
    @*/
    Ox.getDegreesPerMeter = function(lat) {
        return 360 / Ox.EARTH_CIRCUMFERENCE / Math.cos(lat * Math.PI / 180);
    };

    /*@
    Ox.getDistance <f> Returns the distance in meters between two points
        > Ox.getDistance({lat: -45, lng: -90}, {lat: 45, lng: 90}) * 2
        Ox.EARTH_CIRCUMFERENCE
    @*/
    Ox.getDistance = function(point0, point1) {
        var point0 = rad(point0),
            point1 = rad(point1);
        return Math.acos(
    	    Math.sin(point0.lat) * Math.sin(point1.lat) + 
    	    Math.cos(point0.lat) * Math.cos(point1.lat) *
    	    Math.cos(point1.lng - point0.lng)
    	) * Ox.EARTH_RADIUS;
    };

    /*@
    Ox.getLatLngByXY <f> Returns lat/lng for a given x/y on a 1x1 mercator projection
        > Ox.getLatLngByXY({x: 0.5, y: 0.5})
        {lat: 0, lng: 0}
    @*/
    Ox.getLatLngByXY = function(xy) {
        function getVal(val) {
            return (val - 0.5) * 2 * Math.PI;
        }
        return {
            lat: -Ox.deg(Math.atan(Ox.sinh(getVal(xy.y)))),
            lng: Ox.deg(getVal(xy.x))
        }
    };

    /*@
    Ox.getMetersPerDegree <f> Returns meters per degree at a given latitude
        > Ox.getMetersPerDegree(0) * 360
        Ox.EARTH_CIRCUMFERENCE
    @*/
    Ox.getMetersPerDegree = function(lat) {
        return Math.cos(lat * Math.PI / 180) * Ox.EARTH_CIRCUMFERENCE / 360;
    };

    /*@
    Ox.getXYByLatLng <f> Returns x/y on a 1x1 mercator projection for a given lat/lng
        > Ox.getXYByLatLng({lat: 0, lng: 0})
        {x: 0.5, y: 0.5}
    @*/
    Ox.getXYByLatLng = function(latlng) {
        function getVal(val) {
            return (val / (2 * Math.PI) + 0.5)
        }
        return {
            x: getVal(Ox.rad(latlng.lng)),
            y: getVal(Ox.asinh(Math.tan(Ox.rad(-latlng.lat))))
        };
    };

}());

//@ Ox.Line <f> (undocumented)
Ox.Line = function(point0, point1) {

    var self = {
            points: [point0, point1]
        },
        that = this;

    function rad() {
        return self.points.map(function(point) {
            return {
                lat: Ox.rad(point.lat()),
                lng: Ox.rad(point.lng())
            };
        });
    }

    that.getArea = function() {
        
    };

    that.getBearing = function() {
    };

    that.getDistance = function() {
        var points = rad();
        return Math.acos(
    	    Math.sin(point[0].lat) * Math.sin(point[1].lat) + 
    	    Math.cos(point[0].lat) * Math.cos(point[1].lat) *
    	    Math.cos(point[1].lng - point[0].lng)
    	) * Ox.EARTH_RADIUS; 
    };

    that.getMidpoint = function() {
        var points = rad(),
            x = Math.cos(point[1].lat) *
                Math.cos(point[1].lng - point[0].lng),
            y = Math.cos(point[1].lat) *
                Math.sin(point[1].lng - point[0].lng),
            d = Math.sqrt(
                Math.pow(Math.cos(point[0].lat) + x, 2) + Math.pow(y, 2)
            ),
            lat = Ox.deg(
                Math.atan2(Math.sin(points[0].lat) + Math.sin(points[1].lat), d)
            ),
            lng = Ox.deg(
                points[0].lng + Math.atan2(y, math.cos(points[0].lat) + x)
            );
        return new Point(lat, lng);
    };

    that.points = function() {
        
    };

    return that;

};

//@ Ox.Point <f> (undocumented)
Ox.Point = function(lat, lng) {

    var self = {lat: lat, lng: lng},
        that = this;

    that.lat = function() {
        
    };

    that.latlng = function() {
        
    };

    that.lng = function() {
        
    };

    that.getMetersPerDegree = function() {
        return Math.cos(self.lat * Math.PI / 180) *
            Ox.EARTH_CIRCUMFERENCE / 360;
    }

    that.getXY = function() {
        return [
            getXY(Ox.rad(self.lng)),
            getXY(Ox.asinh(Math.tan(Ox.rad(-self.lat))))
        ];
    };

    return that;

};

//@ Ox.Rectangle <f> (undocumented)
Ox.Rectangle = function(point0, point1) {

    var self = {
            points: [
                new Point(
                    Math.min(point0.lat(), point1.lat()),
                    point0.lng()
                ),
                new Point(
                    Math.max(point0.lat(), point1.lat()),
                    point1.lng()
                )
            ]
        },
        that = this;

    that.contains = function(rectangle) {
        
    }

    that.crossesDateline = function() {
        return self.points[0].lng > self.points[1].lng;
    }

    that.getCenter = function() {
        return new Ox.Line(self.points[0], self.points[1]).getMidpoint();
    };

    that.intersects = function(rectangle) {
        
    };

    return that;

};