


/*
 * The JTS Topology Suite is a collection of Java classes that
 * implement the fundamental operations required to validate a given
 * geo-spatial data set to a known topological specification.
 *
 * Copyright (C) 2001 Vivid Solutions
 *
 * 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; either
 * version 2.1 of the License, or (at your option) any later version.
 *
 * 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.
 *
 * You should have received a copy of the GNU Lesser General Public
 * License along with this library; if not, write to the Free Software
 * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
 *
 * For more information, contact:
 *
 *     Vivid Solutions
 *     Suite #1A
 *     2328 Government Street
 *     Victoria BC  V8T 5G5
 *     Canada
 *
 *     (250)385-6040
 *     www.vividsolutions.com
 */
package com.vividsolutions.jts.geomgraph;

import java.util.*;
import com.vividsolutions.jts.algorithm.*;
import com.vividsolutions.jts.algorithm.locate.*;
import com.vividsolutions.jts.geom.*;
import com.vividsolutions.jts.geomgraph.index.*;
import com.vividsolutions.jts.util.*;

/**
 * A GeometryGraph is a graph that models a given Geometry
 * @version 1.7
 */
public class GeometryGraph
  extends PlanarGraph
{
/**
 * This method implements the Boundary Determination Rule
 * for determining whether
 * a component (node or edge) that appears multiple times in elements
 * of a MultiGeometry is in the boundary or the interior of the Geometry
 * <br>
 * The SFS uses the "Mod-2 Rule", which this function implements
 * <br>
 * An alternative (and possibly more intuitive) rule would be
 * the "At Most One Rule":
 *    isInBoundary = (componentCount == 1)
 */
/*
  public static boolean isInBoundary(int boundaryCount)
  {
    // the "Mod-2 Rule"
    return boundaryCount % 2 == 1;
  }
  public static int determineBoundary(int boundaryCount)
  {
    return isInBoundary(boundaryCount) ? Location.BOUNDARY : Location.INTERIOR;
  }
*/

  public static int determineBoundary(BoundaryNodeRule boundaryNodeRule, int boundaryCount)
  {
    return boundaryNodeRule.isInBoundary(boundaryCount)
        ? Location.BOUNDARY : Location.INTERIOR;
  }

  private Geometry parentGeom;

  /**
   * The lineEdgeMap is a map of the linestring components of the
   * parentGeometry to the edges which are derived from them.
   * This is used to efficiently perform findEdge queries
   */
  private Map lineEdgeMap = new HashMap();

  private BoundaryNodeRule boundaryNodeRule = null;

  /**
   * If this flag is true, the Boundary Determination Rule will used when deciding
   * whether nodes are in the boundary or not
   */
  private boolean useBoundaryDeterminationRule = true;
  private int argIndex;  // the index of this geometry as an argument to a spatial function (used for labelling)
  private Collection boundaryNodes;
  private boolean hasTooFewPoints = false;
  private Coordinate invalidPoint = null;

  private PointOnGeometryLocator areaPtLocator = null;
  // for use if geometry is not Polygonal
  private final PointLocator ptLocator = new PointLocator();
  
  private EdgeSetIntersector createEdgeSetIntersector()
  {
  // various options for computing intersections, from slowest to fastest

  //private EdgeSetIntersector esi = new SimpleEdgeSetIntersector();
  //private EdgeSetIntersector esi = new MonotoneChainIntersector();
  //private EdgeSetIntersector esi = new NonReversingChainIntersector();
  //private EdgeSetIntersector esi = new SimpleSweepLineIntersector();
  //private EdgeSetIntersector esi = new MCSweepLineIntersector();

    //return new SimpleEdgeSetIntersector();
    return new SimpleMCSweepLineIntersector();
  }

  public GeometryGraph(int argIndex, Geometry parentGeom)
  {
    this(argIndex, parentGeom,
         BoundaryNodeRule.OGC_SFS_BOUNDARY_RULE
         );
  }

  public GeometryGraph(int argIndex, Geometry parentGeom, BoundaryNodeRule boundaryNodeRule) {
    this.argIndex = argIndex;
    this.parentGeom = parentGeom;
    this.boundaryNodeRule = boundaryNodeRule;
    if (parentGeom != null) {
//      precisionModel = parentGeom.getPrecisionModel();
//      SRID = parentGeom.getSRID();
      add(parentGeom);
    }
  }

  /**
   * This constructor is used by clients that wish to add Edges explicitly,
   * rather than adding a Geometry.  (An example is BufferOp).
   */
  // no longer used
//  public GeometryGraph(int argIndex, PrecisionModel precisionModel, int SRID) {
//    this(argIndex, null);
//    this.precisionModel = precisionModel;
//    this.SRID = SRID;
//  }
//  public PrecisionModel getPrecisionModel()
//  {
//    return precisionModel;
//  }
//  public int getSRID() { return SRID; }

  public boolean hasTooFewPoints() { return hasTooFewPoints; }

  public Coordinate getInvalidPoint() { return invalidPoint; }

  public Geometry getGeometry() { return parentGeom; }

  public BoundaryNodeRule getBoundaryNodeRule() { return boundaryNodeRule; }

  public Collection getBoundaryNodes()
  {
    if (boundaryNodes == null)
      boundaryNodes = nodes.getBoundaryNodes(argIndex);
    return boundaryNodes;
  }

  public Coordinate[] getBoundaryPoints()
  {
    Collection coll = getBoundaryNodes();
    Coordinate[] pts = new Coordinate[coll.size()];
    int i = 0;
    for (Iterator it = coll.iterator(); it.hasNext(); ) {
      Node node = (Node) it.next();
      pts[i++] = (Coordinate) node.getCoordinate().clone();
    }
    return pts;
  }

  public Edge findEdge(LineString line)
  {
    return (Edge) lineEdgeMap.get(line);
  }

  public void computeSplitEdges(List edgelist)
  {
    for (Iterator i = edges.iterator(); i.hasNext(); ) {
      Edge e = (Edge) i.next();
      e.eiList.addSplitEdges(edgelist);
    }
  }
  private void add(Geometry g)
  {
    if (g.isEmpty()) return;

    // check if this Geometry should obey the Boundary Determination Rule
    // all collections except MultiPolygons obey the rule
    if (g instanceof MultiPolygon)
      useBoundaryDeterminationRule = false;

    if (g instanceof Polygon)                 addPolygon((Polygon) g);
                        // LineString also handles LinearRings
    else if (g instanceof LineString)         addLineString((LineString) g);
    else if (g instanceof Point)              addPoint((Point) g);
    else if (g instanceof MultiPoint)         addCollection((MultiPoint) g);
    else if (g instanceof MultiLineString)    addCollection((MultiLineString) g);
    else if (g instanceof MultiPolygon)       addCollection((MultiPolygon) g);
    else if (g instanceof GeometryCollection) addCollection((GeometryCollection) g);
    else  throw new UnsupportedOperationException(g.getClass().getName());
  }

  private void addCollection(GeometryCollection gc)
  {
    for (int i = 0; i < gc.getNumGeometries(); i++) {
      Geometry g = gc.getGeometryN(i);
      add(g);
    }
  }
  /**
   * Add a Point to the graph.
   */
  private void addPoint(Point p)
  {
    Coordinate coord = p.getCoordinate();
    insertPoint(argIndex, coord, Location.INTERIOR);
  }
  
  /**
   * Adds a polygon ring to the graph.
   * Empty rings are ignored.
   * 
   * The left and right topological location arguments assume that the ring is oriented CW.
   * If the ring is in the opposite orientation,
   * the left and right locations must be interchanged.
   */
  private void addPolygonRing(LinearRing lr, int cwLeft, int cwRight)
  {
  	// don't bother adding empty holes
  	if (lr.isEmpty()) return;
  	
    Coordinate[] coord = CoordinateArrays.removeRepeatedPoints(lr.getCoordinates());

    if (coord.length < 4) {
      hasTooFewPoints = true;
      invalidPoint = coord[0];
      return;
    }

    int left  = cwLeft;
    int right = cwRight;
    if (CGAlgorithms.isCCW(coord)) {
      left = cwRight;
      right = cwLeft;
    }
    Edge e = new Edge(coord,
                        new Label(argIndex, Location.BOUNDARY, left, right));
    lineEdgeMap.put(lr, e);

    insertEdge(e);
    // insert the endpoint as a node, to mark that it is on the boundary
    insertPoint(argIndex, coord[0], Location.BOUNDARY);
  }

  private void addPolygon(Polygon p)
  {
    addPolygonRing(
            (LinearRing) p.getExteriorRing(),
            Location.EXTERIOR,
            Location.INTERIOR);

    for (int i = 0; i < p.getNumInteriorRing(); i++) {
    	LinearRing hole = (LinearRing) p.getInteriorRingN(i);
    	
      // Holes are topologically labelled opposite to the shell, since
      // the interior of the polygon lies on their opposite side
      // (on the left, if the hole is oriented CW)
      addPolygonRing(
      		hole,
          Location.INTERIOR,
          Location.EXTERIOR);
    }
  }

  private void addLineString(LineString line)
  {
    Coordinate[] coord = CoordinateArrays.removeRepeatedPoints(line.getCoordinates());

    if (coord.length < 2) {
      hasTooFewPoints = true;
      invalidPoint = coord[0];
      return;
    }

    // add the edge for the LineString
    // line edges do not have locations for their left and right sides
    Edge e = new Edge(coord, new Label(argIndex, Location.INTERIOR));
    lineEdgeMap.put(line, e);
    insertEdge(e);
    /**
     * Add the boundary points of the LineString, if any.
     * Even if the LineString is closed, add both points as if they were endpoints.
     * This allows for the case that the node already exists and is a boundary point.
     */
    Assert.isTrue(coord.length >= 2, "found LineString with single point");
    insertBoundaryPoint(argIndex, coord[0]);
    insertBoundaryPoint(argIndex, coord[coord.length - 1]);

  }

  /**
   * Add an Edge computed externally.  The label on the Edge is assumed
   * to be correct.
   */
  public void addEdge(Edge e)
  {
    insertEdge(e);
    Coordinate[] coord = e.getCoordinates();
    // insert the endpoint as a node, to mark that it is on the boundary
    insertPoint(argIndex, coord[0], Location.BOUNDARY);
    insertPoint(argIndex, coord[coord.length - 1], Location.BOUNDARY);
  }

  /**
   * Add a point computed externally.  The point is assumed to be a
   * Point Geometry part, which has a location of INTERIOR.
   */
  public void addPoint(Coordinate pt)
  {
    insertPoint(argIndex, pt, Location.INTERIOR);
  }

  /**
   * Compute self-nodes, taking advantage of the Geometry type to
   * minimize the number of intersection tests.  (E.g. rings are
   * not tested for self-intersection, since they are assumed to be valid).
   * @param li the LineIntersector to use
   * @param computeRingSelfNodes if <false>, intersection checks are optimized to not test rings for self-intersection
   * @return the SegmentIntersector used, containing information about the intersections found
   */
  public SegmentIntersector computeSelfNodes(LineIntersector li, boolean computeRingSelfNodes)
  {
    SegmentIntersector si = new SegmentIntersector(li, true, false);
    EdgeSetIntersector esi = createEdgeSetIntersector();
    // optimized test for Polygons and Rings
    if (! computeRingSelfNodes
        && (parentGeom instanceof LinearRing
        || parentGeom instanceof Polygon
        || parentGeom instanceof MultiPolygon)) {
      esi.computeIntersections(edges, si, false);
    }
    else {
      esi.computeIntersections(edges, si, true);
    }
//System.out.println("SegmentIntersector # tests = " + si.numTests);
    addSelfIntersectionNodes(argIndex);
    return si;
  }

  public SegmentIntersector computeEdgeIntersections(
    GeometryGraph g,
    LineIntersector li,
    boolean includeProper)
  {
    SegmentIntersector si = new SegmentIntersector(li, includeProper, true);
    si.setBoundaryNodes(this.getBoundaryNodes(), g.getBoundaryNodes());

    EdgeSetIntersector esi = createEdgeSetIntersector();
    esi.computeIntersections(edges, g.edges, si);
/*
for (Iterator i = g.edges.iterator(); i.hasNext();) {
Edge e = (Edge) i.next();
Debug.print(e.getEdgeIntersectionList());
}
*/
    return si;
  }

  private void insertPoint(int argIndex, Coordinate coord, int onLocation)
  {
    Node n = nodes.addNode(coord);
    Label lbl = n.getLabel();
    if (lbl == null) {
      n.label = new Label(argIndex, onLocation);
    }
    else
      lbl.setLocation(argIndex, onLocation);
  }

  /**
   * Adds candidate boundary points using the current {@link BoundaryNodeRule}.
   * This is used to add the boundary
   * points of dim-1 geometries (Curves/MultiCurves).
   */
  private void insertBoundaryPoint(int argIndex, Coordinate coord)
  {
    Node n = nodes.addNode(coord);
    // nodes always have labels
    Label lbl = n.getLabel();
    // the new point to insert is on a boundary
    int boundaryCount = 1;
    // determine the current location for the point (if any)
    int loc = Location.NONE;
    loc = lbl.getLocation(argIndex, Position.ON);
    if (loc == Location.BOUNDARY) boundaryCount++;

    // determine the boundary status of the point according to the Boundary Determination Rule
    int newLoc = determineBoundary(boundaryNodeRule, boundaryCount);
    lbl.setLocation(argIndex, newLoc);
  }

  private void addSelfIntersectionNodes(int argIndex)
  {
    for (Iterator i = edges.iterator(); i.hasNext(); ) {
      Edge e = (Edge) i.next();
      int eLoc = e.getLabel().getLocation(argIndex);
      for (Iterator eiIt = e.eiList.iterator(); eiIt.hasNext(); ) {
        EdgeIntersection ei = (EdgeIntersection) eiIt.next();
        addSelfIntersectionNode(argIndex, ei.coord, eLoc);
      }
    }
  }
  /**
   * Add a node for a self-intersection.
   * If the node is a potential boundary node (e.g. came from an edge which
   * is a boundary) then insert it as a potential boundary node.
   * Otherwise, just add it as a regular node.
   */
  private void addSelfIntersectionNode(int argIndex, Coordinate coord, int loc)
  {
    // if this node is already a boundary node, don't change it
    if (isBoundaryNode(argIndex, coord)) return;
    if (loc == Location.BOUNDARY && useBoundaryDeterminationRule)
        insertBoundaryPoint(argIndex, coord);
    else
      insertPoint(argIndex, coord, loc);
  }

  // MD - experimental for now
  /**
   * Determines the {@link Location} of the given {@link Coordinate}
   * in this geometry.
   * 
   * @param p the point to test
   * @return the location of the point in the geometry
   */
  public int locate(Coordinate pt)
  {
  	if (parentGeom instanceof Polygonal && parentGeom.getNumGeometries() > 50) {
  		// lazily init point locator
  		if (areaPtLocator == null) {
  			areaPtLocator = new IndexedPointInAreaLocator(parentGeom);
  		}
  		return areaPtLocator.locate(pt);
  	}
  	return ptLocator.locate(pt, parentGeom);
  }
}
