/*
 * 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.operation.union;

import java.util.*;
import com.vividsolutions.jts.geom.*;
import com.vividsolutions.jts.geom.util.*;
import com.vividsolutions.jts.index.strtree.STRtree;


/**
 * Provides an efficient method of unioning a collection of 
 * {@link Polygonal} geometrys.
 * This algorithm is faster and likely more robust than
 * the simple iterated approach of 
 * repeatedly unioning each polygon to a result geometry.
 * <p>
 * The <tt>buffer(0)</tt> trick is sometimes faster, but can be less robust and 
 * can sometimes take an exceptionally long time to complete.
 * This is particularly the case where there is a high degree of overlap
 * between the polygons.  In this case, <tt>buffer(0)</tt> is forced to compute
 * with <i>all</i> line segments from the outset, 
 * whereas cascading can eliminate many segments
 * at each stage of processing.
 * The best case for buffer(0) is the trivial case
 * where there is <i>no</i> overlap between the input geometries. 
 * However, this case is likely rare in practice.
 * 
 * @author Martin Davis
 *
 */
public class CascadedPolygonUnion 
{
	/**
	 * Computes the union of
	 * a collection of {@link Polygonal} {@link Geometry}s.
	 * 
	 * @param polys a collection of {@link Polygonal} {@link Geometry}s
	 */
	public static Geometry union(Collection polys)
	{
		CascadedPolygonUnion op = new CascadedPolygonUnion(polys);
		return op.union();
	}
	
	private Collection inputPolys;
	private GeometryFactory geomFactory = null;
	
	/**
	 * Creates a new instance to union
	 * the given collection of {@link Geometry}s.
	 * 
	 * @param geoms a collection of {@link Polygonal} {@link Geometry}s
	 */
	public CascadedPolygonUnion(Collection polys)
	{
		this.inputPolys = polys;
	}
	
  /**
   * The effectiveness of the index is somewhat sensitive
   * to the node capacity.  
   * Testing indicates that a smaller capacity is better.
   * For an STRtree, 4 is probably a good number (since
   * this produces 2x2 "squares").
   */
  private static final int STRTREE_NODE_CAPACITY = 4;
  
	/**
	 * Computes the union of the input geometries.
	 * 
	 * @return the union of the input geometries
	 * @return null if no input geometries were provided
	 */
	public Geometry union()
	{
		if (inputPolys.isEmpty())
			return null;
		geomFactory = ((Geometry) inputPolys.iterator().next()).getFactory();
		
		/**
		 * A spatial index to organize the collection
		 * into groups of close geometries.
		 * This makes unioning more efficient, since vertices are more likely 
		 * to be eliminated on each round.
		 */
//    STRtree index = new STRtree();
    STRtree index = new STRtree(STRTREE_NODE_CAPACITY);
    for (Iterator i = inputPolys.iterator(); i.hasNext(); ) {
      Geometry item = (Geometry) i.next();
      index.insert(item.getEnvelopeInternal(), item);
    }
    List itemTree = index.itemsTree();

//    printItemEnvelopes(itemTree);
    
    Geometry unionAll = unionTree(itemTree);
    return unionAll;
	}
	
  private Geometry unionTree(List geomTree)
  {
    /**
     * Recursively unions all subtrees in the list into single geometries.
     * The result is a list of Geometrys only
     */
    List geoms = reduceToGeometries(geomTree);
//    Geometry union = bufferUnion(geoms);
    Geometry union = binaryUnion(geoms);
    
    // print out union (allows visualizing hierarchy)
//    System.out.println(union);
    
    return union;
//    return repeatedUnion(geoms);
//    return buffer0Union(geoms);
    
  }

  //========================================================
  /*
   * The following methods are for experimentation only
   */
  
  private Geometry repeatedUnion(List geoms)
  {
  	Geometry union = null;
  	for (Iterator i = geoms.iterator(); i.hasNext(); ) {
  		Geometry g = (Geometry) i.next();
  		if (union == null)
  			union = (Geometry) g.clone();
  		else
  			union = union.union(g);
  	}
  	return union;
  }
  
  private Geometry bufferUnion(List geoms)
  {
  	GeometryFactory factory = ((Geometry) geoms.get(0)).getFactory();
  	Geometry gColl = factory.buildGeometry(geoms);
  	Geometry unionAll = gColl.buffer(0.0);
    return unionAll;
  }
  
  private Geometry bufferUnion(Geometry g0, Geometry g1)
  {
  	GeometryFactory factory = g0.getFactory();
  	Geometry gColl = factory.createGeometryCollection(new Geometry[] { g0, g1 } );
  	Geometry unionAll = gColl.buffer(0.0);
    return unionAll;
  }
  
  //=======================================

  /**
   * Unions a list of geometries 
   * by treating the list as a flattened binary tree,
   * and performing a cascaded union on the tree.
   */
  private Geometry binaryUnion(List geoms)
  {
  	return binaryUnion(geoms, 0, geoms.size());
  }
  
  /**
   * Unions a section of a list using a recursive binary union on each half
   * of the section.
   * 
   * @param geoms
   * @param start
   * @param end
   * @return the union of the list section
   */
  private Geometry binaryUnion(List geoms, int start, int end)
  {
  	if (end - start <= 1) {
  		Geometry g0 = getGeometry(geoms, start);
  		return unionSafe(g0, null);
  	}
  	else if (end - start == 2) {
  		return unionSafe(getGeometry(geoms, start), getGeometry(geoms, start + 1));
  	}
  	else {
  		// recurse on both halves of the list
  		int mid = (end + start) / 2;
  		Geometry g0 = binaryUnion(geoms, start, mid);
  		Geometry g1 = binaryUnion(geoms, mid, end);
  		return unionSafe(g0, g1);
  	}
  }
  
  private static Geometry getGeometry(List list, int index)
  {
  	if (index >= list.size()) return null;
  	return (Geometry) list.get(index);
  }
  
  /**
   * Reduces a tree of geometries to a list of geometries
   * by recursively unioning the subtrees in the list.
   * 
   * @param geomTree a tree-structured list of geometries
   * @return a list of Geometrys
   */
  private List reduceToGeometries(List geomTree)
  {
    List geoms = new ArrayList();
    for (Iterator i = geomTree.iterator(); i.hasNext(); ) {
      Object o = i.next();
      Geometry geom = null;
      if (o instanceof List) {
        geom = unionTree((List) o);
      }
      else if (o instanceof Geometry) {
        geom = (Geometry) o;
      }
      geoms.add(geom);
    }
    return geoms;
  }
  
  /**
   * Computes the union of two geometries, 
   * either of both of which may be null.
   * 
   * @param g0 a Geometry
   * @param g1 a Geometry
   * @return the union of the input(s)
   * @return null if both inputs are null
   */
  private Geometry unionSafe(Geometry g0, Geometry g1)
  {
  	if (g0 == null && g1 == null)
  		return null;

  	if (g0 == null)
  		return (Geometry) g1.clone();
  	if (g1 == null)
  		return (Geometry) g0.clone();
  	
  	// what if both are null?  Maybe return empty GC?
  	
  	return unionOptimized(g0, g1);
  }
  
  private Geometry unionOptimized(Geometry g0, Geometry g1)
  {
  	Envelope g0Env = g0.getEnvelopeInternal();
  	Envelope g1Env = g1.getEnvelopeInternal();
  	//*
  	if (! g0Env.intersects(g1Env))
  	{
  		Geometry combo = GeometryCombiner.combine(g0, g1);
//   		System.out.println("Combined");
//  		System.out.println(combo);
  		return combo;
  	}
  	//*/
//  	System.out.println(g0.getNumGeometries() + ", " + g1.getNumGeometries());
  	
  	if (g0.getNumGeometries() <= 1 && g1.getNumGeometries() <= 1)
  		return unionActual(g0, g1);
  	
  	// for testing...
//  	if (true) return g0.union(g1);
  	
  	Envelope commonEnv = g0Env.intersection(g1Env);
  	return unionUsingEnvelopeIntersection(g0, g1, commonEnv);
  	
//  	return UnionInteracting.union(g0, g1);
  }
  
  
  
  /**
   * Unions two polygonal geometries.
   * The case of MultiPolygons is optimized to union only 
   * the polygons which lie in the intersection of the two geometry's envelopes.
   * Polygons outside this region can simply be combined with the union result,
   * which is potentially much faster.
   * This case is likely to occur often during cascaded union, and may also
   * occur in real world data (such as unioning data for parcels on different street blocks).
   * 
   * @param g0 a polygonal geometry
   * @param g1 a polygonal geometry
   * @param common the intersection of the envelopes of the inputs
   * @return the union of the inputs
   */
  private Geometry unionUsingEnvelopeIntersection(Geometry g0, Geometry g1, Envelope common)
  {
  	List disjointPolys = new ArrayList();
  	
  	Geometry g0Int = extractByEnvelope(common, g0, disjointPolys);
  	Geometry g1Int = extractByEnvelope(common, g1, disjointPolys);
  	
//  	System.out.println("# geoms in common: " + intersectingPolys.size());
  	Geometry union = unionActual(g0Int, g1Int);
  	
  	disjointPolys.add(union);
  	Geometry overallUnion = GeometryCombiner.combine(disjointPolys);
  	
  	return overallUnion;
  }
  
  private Geometry extractByEnvelope(Envelope env, Geometry geom, 
  		List disjointGeoms)
  {
  	List intersectingGeoms = new ArrayList();
  	for (int i = 0; i < geom.getNumGeometries(); i++) { 
  		Geometry elem = geom.getGeometryN(i);
  		if (elem.getEnvelopeInternal().intersects(env))
  			intersectingGeoms.add(elem);
  		else
  			disjointGeoms.add(elem);
  	}
  	return geomFactory.buildGeometry(intersectingGeoms);
  }
  
  /**
   * Encapsulates the actual unioning of two polygonal geometries.
   * 
   * @param g0
   * @param g1
   * @return
   */
  private Geometry unionActual(Geometry g0, Geometry g1)
  {
  	/*
  	System.out.println(g0.getNumGeometries() + ", " + g1.getNumGeometries());
 	
  	if (g0.getNumGeometries() > 5) {
  		System.out.println(g0);
  		System.out.println(g1);
  	}
  	*/
  	
  	//return bufferUnion(g0, g1);
  	return g0.union(g1);
  }
}
