/* -*-c++-*- */
/* osgEarth - Dynamic map generation toolkit for OpenSceneGraph
 * Copyright 2008-2010 Pelican Mapping
 * http://osgearth.org
 *
 * osgEarth is free software; you can redistribute it and/or modify
 * it under the terms of the GNU Lesser General Public License as published by
 * the Free Software Foundation; either version 2 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public License
 * along with this program.  If not, see <http://www.gnu.org/licenses/>
 */
#ifndef OSGEARTH_GEODATA_H
#define OSGEARTH_GEODATA_H 1

#include <osg/Referenced>
#include <osg/Image>
#include <osg/Shape>
#include <osgTerrain/Locator>
#include <osgEarth/Common>
#include <osgEarth/SpatialReference>
#include <osgEarth/VerticalSpatialReference>
#include <osgEarth/HeightFieldUtils>
#include <osgEarth/Units>

namespace osgEarth
{
    /**
     * An "anonymous" bounding extent (i.e., no geo reference information)
     */
    class OSGEARTH_EXPORT Bounds : public osg::BoundingBoxImpl<osg::Vec3d>
    {
    public:
        Bounds();
        Bounds(double xmin, double ymin, double xmax, double ymax );

        double width() const;
        double height() const;
        double depth() const;
        bool contains(double x, double y ) const;
        Bounds unionWith(const Bounds& rhs) const; 
        void expandBy( double x, double y );
        void expandBy( double x, double y, double z );
        void expandBy( const Bounds& rhs );
        osg::Vec2d center2d() const;
    };

    /**
     * A georeferenced extent. A bounding box that is aligned with a
     * spatial reference's coordinate system.
     *
     * TODO: this class needs better integrated support for geographic extents
     * that cross the date line.
     */
    class OSGEARTH_EXPORT GeoExtent
    {
    public:
        /** Default ctor creates an "invalid" extent */
        GeoExtent(); 

        /** Contructs a valid extent */
        GeoExtent(
            const SpatialReference* srs,
            double xmin = FLT_MAX, double ymin = FLT_MAX,
            double xmax = -FLT_MAX, double ymax = -FLT_MAX );

        /** Copy ctor */
        GeoExtent( const GeoExtent& rhs );

        /** create from Bounds object */
        GeoExtent( const SpatialReference* srs, const Bounds& bounds );

        bool operator == ( const GeoExtent& rhs ) const;
        bool operator != ( const GeoExtent& rhs ) const;

        /** Gets the spatial reference system underlying this extent. */
        const SpatialReference* getSRS() const;

        double xMin() const { return _xmin; }
        double& xMin() { return _xmin; }
        double yMin() const { return _ymin; }
        double& yMin() { return _ymin; }
        double xMax() const { return _xmax; }
        double& xMax() { return _xmax; }
        double yMax() const { return _ymax; }
        double& yMax() { return _ymax; }

        double width() const;
        double height() const;

        void getCentroid( double& out_x, double& out_y ) const;

        /**
         * Returns true is that extent is in a Geographic (lat/long) SRS that spans
         * the international date line.
         */
        bool crossesDateLine() const;

        /**
         * Returns the raw bounds in a single function call
         */
        void getBounds(double &xmin, double &ymin, double &xmax, double &ymax) const;

        /** True if this object defines a real, valid extent with positive area */
        bool isValid() const;
        bool defined() const { return isValid(); }

        /**
         * If this extent crosses the international date line, populates two extents, one for
         * each side, and returns true. Otherwise returns false and leaves the reference
         * parameters untouched.
         */
        bool splitAcrossDateLine( GeoExtent& first, GeoExtent& second ) const;

        /**
         * Returns this extent transformed into another spatial reference.
         */
        GeoExtent transform( const SpatialReference* to_srs ) const;

        /**
         * Returns true if the specified point falls within the bounds of the extent.
         *
         * @param x, y
         *      Coordinates to test
         * @param xy_srs
         *      SRS of input x and y coordinates; if null, the method assumes x and y
         *      are in the same SRS as this object.
         */
        bool contains(double x, double y, const SpatialReference* xy_srs =0L) const;

        /**
         * Returns TRUE if this extent intersects another extent.
         */
        bool intersects( const GeoExtent& rhs ) const;

        /** Direct access to the anonymous bounding box */
        Bounds bounds() const;

        /**
         * Grow this extent to include the specified point (which is assumed to be
         * in the extent's SRS.
         */
        void expandToInclude( double x, double y );

        /**
         * Grow this extent to include the specified GeoExtent (which is assumed to be
         * in the extent's SRS.
         */
        void expandToInclude( const GeoExtent& rhs );
        
        /**
         * Intersect this extent with another extent in the same SRS and return the
         * result.
         */
        GeoExtent intersectionSameSRS( const GeoExtent& rhs ) const;

        /**
         * Returns a human-readable string containing the extent data (without the SRS)
         */
        std::string toString() const;

        /**
         *Inflates this GeoExtent by the given ratios
         */
        void scale(double x_scale, double y_scale);

		/**
		 * Expands the extent by x and y.
		 */
		void expand( double x, double y );

        /**
         *Gets the area of this GeoExtent
         */
        double area() const;

    public:
        static GeoExtent INVALID;

    private:
        osg::ref_ptr<const SpatialReference> _srs;
        double _xmin, _ymin, _xmax, _ymax;
    };

    /**
     * A geospatial area with tile data LOD extents
     */
    class OSGEARTH_EXPORT DataExtent : public GeoExtent
    {
    public:
        DataExtent(const GeoExtent& extent, unsigned int minLevel, unsigned int maxLevel);

        /** The minimum LOD of the extent */
        unsigned int getMinLevel() const;

        /** The maximum LOD of the extent */
        unsigned int getMaxLevel() const;

    private:
        unsigned int _minLevel;
        unsigned int _maxLevel;
    };

    typedef std::vector< DataExtent > DataExtentList;


    /**
     * A georeferenced image; i.e. an osg::Image and an associated GeoExtent with SRS.
     */
    class OSGEARTH_EXPORT GeoImage
    {
    public:
        /** Construct an empty (invalid) geoimage. */
        GeoImage();

        /**
         * Constructs a new goereferenced image.
         */
        GeoImage( osg::Image* image, const GeoExtent& extent );

        static GeoImage INVALID;

    public:
        /**
         * True if this is a valid geo image. 
         */
        bool valid() const { return _image.valid(); }

        /**
         * Gets a pointer to the underlying OSG image.
         */
        osg::Image* getImage() const;

        /**
         * Gets the geospatial extent of the image.
         */
        const GeoExtent& getExtent() const;

        /**
         * Shortcut to get the spatial reference system describing
         * the projection of the image.
         */
        const SpatialReference* getSRS() const;

        /**
         * Crops the image to a new geospatial extent. 
         *
         * @param extent
         *      New extent to which to crop the image.
         * @param exact
         *      If "exact" is true, the output image will have exactly the extents requested;
         *      this process may require resampling and will therefore be more expensive. If
         *      "exact" is false, we do a simple crop of the image that is rounded to the nearest
         *      pixel. The resulting extent will be close but usually not exactly what was
         *      requested - however, this method is faster.
         * @param width, height
         *      New pixel size for the output image. By default, the method will automatically
         *      calculate a new pixel size.
         */
        GeoImage crop( 
            const GeoExtent& extent,
            bool exact = false,
            unsigned int width = 0,
            unsigned int height = 0) const;

        /**
         * Warps the image into a new spatial reference system.
         *
         * @param to_srs
         *      SRS into which to warp the image.
         * @param to_extent
         *      Supply this extent if you wish to warp AND crop the image in one step. This is
         *      faster than calling reproject() and then crop().
         * @param width, height
         *      New pixel size for the output image. Be default, the method will automatically
         *      calculate a new pixel size.
         */
        GeoImage reproject(
            const SpatialReference* to_srs,
            const GeoExtent* to_extent = 0,
            unsigned int width = 0,
            unsigned int height = 0) const;

        /**
         * Adds a one-pixel transparent border around an image.
         */
        GeoImage addTransparentBorder(
            bool leftBorder=true, 
            bool rightBorder=true, 
            bool bottomBorder=true, 
            bool topBorder=true);

        /**
         * Returns the underlying OSG image and releases the reference pointer.
         */
        osg::Image* takeImage();

		/**
		 * Gets the units per pixel of this geoimage
		 */
		double getUnitsPerPixel() const;

    private:
        osg::ref_ptr<osg::Image> _image;
        GeoExtent _extent;
    };

    typedef std::vector<GeoImage> GeoImageVector;

    /**
     * A georeferenced heightfield.
     */
    class OSGEARTH_EXPORT GeoHeightField
    {
    public:
        /** Constructs an empty (invalid) heightfield. */
        GeoHeightField();

        /**
         * Constructs a new georeferenced heightfield.
         */
        GeoHeightField(
            osg::HeightField* heightField,
            const GeoExtent& extent,
            const VerticalSpatialReference* vsrs);

        static GeoHeightField INVALID;

        /**
         * True if this is a valid heightfield. 
         */
        bool valid() const { return _heightField.valid(); }

        /**
         * Gets the elevation value at a specified point.
         *
         * @param srs
         *      Spatial reference of the query coordinates. (If you pass in NULL, the method
         *      will assume that the SRS is equivalent to that of the GeoHeightField. Be sure
         *      this is case of you will get incorrect results.)
         * @param x, y
         *      Coordinates at which to query the elevation value.
         * @param interp
         *      Interpolation method for the elevation query.
         * @param outputVSRS
         *      Convert the output elevation value to this VSRS (NULL to ignore)
         * @param out_elevation
         *      Output: the elevation value
         * @return
         *      True if the elevation query was succesful; false if not (e.g. if the query
         *      fell outside the geospatial extent of the heightfield)
         */
        bool getElevation(
            const SpatialReference* inputSRS, 
            double x, double y,
            ElevationInterpolation interp,
            const VerticalSpatialReference* outputVSRS,
            float& out_elevation ) const;
        
        /**
         * Subsamples the heightfield, returning a new heightfield corresponding to
         * the destEx extent. The destEx must be a smaller, inset area of sourceEx.
         */
        GeoHeightField createSubSample( const GeoExtent& destEx, ElevationInterpolation interpolation) const;

        /**
         * Gets the geospatial extent of the heightfield.
         */
        const GeoExtent& getExtent() const;

        /**
         * Gets a pointer to the underlying OSG heightfield.
         */
        const osg::HeightField* getHeightField() const;
        osg::HeightField* getHeightField();

        /**
         * Gets a pointer to the underlying OSG heightfield, and releases the internal reference.
         */
        osg::HeightField* takeHeightField();

    protected:
        osg::ref_ptr<osg::HeightField> _heightField;
        GeoExtent _extent;
        osg::ref_ptr<const VerticalSpatialReference> _vsrs;
    };

	typedef std::vector<GeoHeightField> GeoHeightFieldVector;

    
    /**
     * A representation of the surface of the earth based on a grid of
     * height values that are relative to a reference ellipsoid.
     */
    class OSGEARTH_EXPORT Geoid : public osg::Referenced
    {
    public:
        Geoid();

        /** Gets the readable name of this geoid. */
        void setName( const std::string& value );
        const std::string& getName() const { return _name; }

        /** Sets the underlying heightfield data */
        void setHeightField( const GeoHeightField& hf );

        /** Queries to geoid for the height offset at the specified coordinates. */
        float getOffset(
            double lat_deg, double lon_deg, 
            const ElevationInterpolation& interp =INTERP_BILINEAR) const;

        /** The linear units in which height values are expressed. */
        const Units& getUnits() const { return _units; }
        void setUnits( const Units& value );

        /** Whether this is a valid object to use */
        bool isValid() const { return _valid; }

        /** True if two geoids are mathmatically equivalent. */
        bool isEquivalentTo( const Geoid& rhs ) const;

    private:
        std::string _name;
        GeoHeightField _hf;
        Units _units;
        bool _valid;
        void validate();
    };

}

#endif // OSGEARTH_GEODATA_H
