/*
 *    GeoTools - OpenSource mapping toolkit
 *    http://geotools.org
 *    (C) 2002-2006, GeoTools Project Managment Committee (PMC)
 *    
 *    This library 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;
 *    version 2.1 of the License.
 *
 *    This library 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.
 *
 *    Created on 28 July 2002, 16:03
 */

package org.geotools.filter;

import com.vividsolutions.jts.geom.Coordinate;
import com.vividsolutions.jts.geom.Geometry;
import com.vividsolutions.jts.geom.GeometryCollection;
import com.vividsolutions.jts.geom.LineString;
import com.vividsolutions.jts.geom.MultiLineString;
import com.vividsolutions.jts.geom.MultiPolygon;
import com.vividsolutions.jts.geom.Polygon;

/**
 *
 * @author  James
 * @source $URL: http://svn.geotools.org/geotools/tags/2.4.1/modules/library/main/src/main/java/org/geotools/filter/AreaFunction.java $
 */
public class AreaFunction extends FunctionExpressionImpl{ 
    
    /** Creates a new instance of AreaFunction */
    public AreaFunction() {
        super("Area");
    }
    
    public Object evaluate(Object feature) {
        org.opengis.filter.expression.Expression geom;
        geom = (org.opengis.filter.expression.Expression)getParameters().get(0);
        Geometry g = (Geometry)geom.evaluate(feature);
    	
    	return new Double( getArea( g ));
    }
    
    public int getArgCount() {
        return 1;
    }
    

    /**
     * Returns the area of a GeometryCollection.
     *
     * @param geometryCollection1 The GeometryCollection for which the
     * area is calulated.
     * @return The total area of all geometries in the collection.
     */
    protected double getArea(GeometryCollection geometryCollection1) {
        double area = 0.0d;
        //double perimeter = 0.0d;
        int numberOfGeometries1 = geometryCollection1.getNumGeometries();
        // Go through geometryCollection1 and sum areas of component geometries
        for (int i = 0; i < numberOfGeometries1; i++) {
            area += getArea(geometryCollection1.getGeometryN(i));
        }
        return area;
    }

    /**
     * Returns.
     * @param geometryCollection The GeometryCollection for which the
     * perimeter is calulated.
     * @return the perimeter of a GeometryCollection.
     */
    protected double getPerimeter(GeometryCollection geometryCollection) {
        double perimeter = 0.0d;
        int numberOfGeometries = geometryCollection.getNumGeometries();
        // Go through geometryCollection and sum perimeters of component 
        // geometries
        for (int i = 0; i < numberOfGeometries; i++) {
            perimeter += getPerimeter(geometryCollection.getGeometryN(i));
        }
        return perimeter;
    }

   /**
     * Calculates and returns the area of the specified geometry.<br>
     * For Polygons, this is the total area inside the external ring less
     * the total of any contained by interior rings.  GeometryCollections
     * (including MultiPolygons) are iterated through so the result is the
     * sum of all polygons anywhere within the collection.
     * Any geometry other than Polgyon or a collection returns 0;
     *
     * @param geometry The Geometry to calculate the area of.
     * @return The total area of the Geometry.
     */
    public double getArea(Geometry geometry) {
        double area = 0.0d;
        if (geometry instanceof GeometryCollection) {
            area += getArea((GeometryCollection) geometry);
        } else if (geometry instanceof MultiPolygon) {
            area += getArea((MultiPolygon) geometry);
        } else if (geometry instanceof Polygon) {
            area += getArea((Polygon) geometry);
        } else {
            area += 0.0d;
        }
        return area;
    }

   /**
     * Calculates and returns the perimeter of the specified geometry.<br>
     * For Polygons, this is the total length of the exterior ring and all
     * internal rings.  For LineStrings the total line length is returned.
     * GeometryCollections are iterated through so the result is the
     * sum of all Polygon and Line geometries anywhere within the collection.
     * Any point geometries return a value of 0;
     *
     * @param geometry The Geometry to calculate the area of.
     * @return The total area of the Geometry.
     */
    public double getPerimeter(Geometry geometry) {
        double perimeter = 0.0d;
        if (geometry instanceof GeometryCollection) {
            perimeter += getPerimeter((GeometryCollection) geometry);
        } else if (geometry instanceof MultiPolygon) {
            perimeter += getPerimeter((MultiPolygon) geometry);
        } else if (geometry instanceof Polygon) {
            perimeter += getPerimeter((Polygon) geometry);
        } else if (geometry instanceof MultiLineString) {
            perimeter += getPerimeter((MultiLineString) geometry);
        } else if (geometry instanceof LineString) {
            perimeter += getPerimeter((LineString) geometry);
        } else {
            perimeter += 0.0d;
        }
        return perimeter;
    }
    
    /**
     * Returns the area of a MultiPolygon.
     * @param multiPolygon the MultiPolygon for which the area is calculated.
     * @return Total area of all polygons in multiPolygon.
     */
    protected double getArea(MultiPolygon multiPolygon) {
        double area = 0.0d;
        int numberOfGeometries = multiPolygon.getNumGeometries();
        for (int i = 0; i < numberOfGeometries; i++) {
            area += getArea(multiPolygon.getGeometryN(i));
        }
        return area;
    }
    
    /**
     * Returns the perimeter of a MultiPolygon.
     * @param multiPolygon the MultiPolygon for which the perimeter is 
     * calculated.
     * @return Total perimeter of all polygons in the multiPolygon.
     */
    protected double getperimeter(MultiPolygon multiPolygon) {
        double perimeter = 0.0d;
        int numberOfGeometries = multiPolygon.getNumGeometries();
        for (int i = 0; i < numberOfGeometries; i++) {
            perimeter += getPerimeter(multiPolygon.getGeometryN(i));
        }
        return perimeter;
    }

    /**
     * Returns the area of a Polygon.
     * @param polygon the Polygon for which the area is calculated.
     * @return The area of the polygon.
     */
    protected double getArea(Polygon polygon) {
        double area = 0.0d;
        double interiorArea = 0.0d;
        Coordinate[] exteriorRingCoordinates = polygon.getExteriorRing().getCoordinates();
        int numberOfExteriorRingCoordinates = exteriorRingCoordinates.length;
        // Calculate the boundingBox of polygon1 aligned with the axes x and y
        double minx = Double.POSITIVE_INFINITY;
        double maxx = Double.NEGATIVE_INFINITY;
        double miny = Double.POSITIVE_INFINITY;
        double maxy = Double.NEGATIVE_INFINITY;
        for (int i = 0; i < numberOfExteriorRingCoordinates; i++) {
            minx = Math.min(minx, exteriorRingCoordinates[i].x);
            maxx = Math.max(maxx, exteriorRingCoordinates[i].x);
            miny = Math.min(miny, exteriorRingCoordinates[i].y);
            maxy = Math.max(maxy, exteriorRingCoordinates[i].y);
        }
        // Calculate area of each trapezoid formed by dropping lines from 
        // each pair of coordinates in exteriorRingCoordinates to the x-axis.
        // x[i]<x[i-1] will contribute a negative area
        for (int i = 0; i < (numberOfExteriorRingCoordinates - 1); i++) {
            area += (((exteriorRingCoordinates[i + 1].x - minx) - 
                    (exteriorRingCoordinates[i].x - minx)) * 
                    (((exteriorRingCoordinates[i + 1].y - miny) + 
                    (exteriorRingCoordinates[i].y - miny)) / 2d));
        }
        area = Math.abs(area);
        // Calculate area of each trapezoid formed by dropping lines
        // from each pair of coordinates in interiorRingCoorinates to the x-axis.
        int numberOfInteriorRings = polygon.getNumInteriorRing();
        int numberOfInteriorRingCoordinates;
        Coordinate[] interiorRingCoordinates;
        for (int i = 0; i < numberOfInteriorRings; i++) {
            interiorArea = 0.0d;
            interiorRingCoordinates = polygon.getInteriorRingN(i).getCoordinates();
            numberOfInteriorRingCoordinates = interiorRingCoordinates.length;
            minx = Double.POSITIVE_INFINITY;
            maxx = Double.NEGATIVE_INFINITY;
            miny = Double.POSITIVE_INFINITY;
            maxy = Double.NEGATIVE_INFINITY;
            for (int j = 0; j < numberOfInteriorRingCoordinates; j++) {
                minx = Math.min(minx, interiorRingCoordinates[j].x);
                maxx = Math.max(maxx, interiorRingCoordinates[j].x);
                miny = Math.min(miny, interiorRingCoordinates[j].y);
                maxy = Math.max(maxy, interiorRingCoordinates[j].y);
            }
            for (int j = 0; j < (numberOfInteriorRingCoordinates - 1); j++) {
                interiorArea += (((interiorRingCoordinates[j + 1].x - minx) - 
                                (interiorRingCoordinates[j].x - minx)) * 
                                (((interiorRingCoordinates[j + 1].y - miny) + 
                                (interiorRingCoordinates[j].y - miny)) / 2d));
            }
            area -= Math.abs(interiorArea);
        }
        return area;
    }

    /**
     * Returns the perimeter of a Polygon.
     * @param polygon the Polygon for which the perimeter is calculated.
     * @return The perimeter of the polygon.
     */
    protected double getPerimeter(Polygon polygon) {
        double perimeter = 0.0d;
        LineString lineString = polygon.getExteriorRing();
        perimeter += getPerimeter(lineString);
        int numberOfHoles = polygon.getNumInteriorRing();
        for (int i = 0; i < numberOfHoles; i++) {
            perimeter += getPerimeter(polygon.getInteriorRingN(i));
        }
        return perimeter;
    }

    /**
     * Returns the perimeter of a MultiLineString.
     * @param multiLineString the MultiLineString for which the perimeter is
     * calculated.
     * @return the total perimter (length) of the lines in multiLineString.
     */
    protected double getPerimeter(MultiLineString multiLineString) {
        double perimeter = 0.0d;
        int numberOfGeometries = multiLineString.getNumGeometries();
        for (int i = 0; i < numberOfGeometries; i++) {
            perimeter += getPerimeter(multiLineString.getGeometryN(i));
        }
        return perimeter;
    }

    /**
     * Returns the perimeter of a LineString.
     * @param lineString the LineString for which the perimeter is calculated.
     * @return the perimeter (length) of the lineString.
     */
    protected double getPerimeter(LineString lineString) {
        double perimeter = 0.0d;
        int numberOfPoints = lineString.getNumPoints();
        Coordinate[] coordinates = lineString.getCoordinates();
        for (int i = 0; i < (numberOfPoints - 1); i++) {
            perimeter += coordinates[i].distance(coordinates[i + 1]);
        }
        return perimeter;
    }

}
