<?php

/**
 * Collection: Abstract class for compound geometries
 *
 * A geometry is a collection if it is made up of other
 * component geometries. Therefore everything but a Point
 * is a Collection. For example a LingString is a collection
 * of Points. A Polygon is a collection of LineStrings etc.
 */
abstract class Collection extends Geometry
{
  public $components = array();

  /**
   * Constructor: Checks and sets component geometries
   *
   * @param array $components array of geometries
   */
  public function __construct($components = array()) {
    if (!is_array($components)) {
      throw new Exception("Component geometries must be passed as an array");
    }
    foreach ($components as $component) {
      if ($component instanceof Geometry) {
        $this->components[] = $component;
      }
      else {
        throw new Exception("Cannot create a collection with non-geometries");
      }
    }
  }

  /**
   * Returns Collection component geometries
   *
   * @return array
   */
  public function getComponents() {
    return $this->components;
  }

  public function centroid() {
    if ($this->isEmpty()) return NULL;

    if ($this->geos()) {
      $geos_centroid = $this->geos()->centroid();
      if ($geos_centroid->typeName() == 'Point') {
        return geoPHP::geosToGeometry($this->geos()->centroid());
      }
    }

    // As a rough estimate, we say that the centroid of a colletion is the centroid of it's envelope
    // @@TODO: Make this the centroid of the convexHull
    // Note: Outside of polygons, geometryCollections and the trivial case of points, there is no standard on what a "centroid" is
    $centroid = $this->envelope()->centroid();

    return $centroid;
  }

  public function getBBox() {
    if ($this->isEmpty()) return NULL;

    if ($this->geos()) {
      $envelope = $this->geos()->envelope();
      if ($envelope->typeName() == 'Point') {
        return geoPHP::geosToGeometry($envelope)->getBBOX();
      }

      $geos_ring = $envelope->exteriorRing();
      return array(
        'maxy' => $geos_ring->pointN(3)->getY(),
        'miny' => $geos_ring->pointN(1)->getY(),
        'maxx' => $geos_ring->pointN(1)->getX(),
        'minx' => $geos_ring->pointN(3)->getX(),
      );
    }

    // Go through each component and get the max and min x and y
    $i = 0;
    foreach ($this->components as $component) {
      $component_bbox = $component->getBBox();

      // On the first run through, set the bbox to the component bbox
      if ($i == 0) {
        $maxx = $component_bbox['maxx'];
        $maxy = $component_bbox['maxy'];
        $minx = $component_bbox['minx'];
        $miny = $component_bbox['miny'];
      }

      // Do a check and replace on each boundary, slowly growing the bbox
      $maxx = $component_bbox['maxx'] > $maxx ? $component_bbox['maxx'] : $maxx;
      $maxy = $component_bbox['maxy'] > $maxy ? $component_bbox['maxy'] : $maxy;
      $minx = $component_bbox['minx'] < $minx ? $component_bbox['minx'] : $minx;
      $miny = $component_bbox['miny'] < $miny ? $component_bbox['miny'] : $miny;
      $i++;
    }

    return array(
      'maxy' => $maxy,
      'miny' => $miny,
      'maxx' => $maxx,
      'minx' => $minx,
    );
  }

  public function asArray() {
    $array = array();
    foreach ($this->components as $component) {
      $array[] = $component->asArray();
    }
    return $array;
  }

  public function area() {
    if ($this->geos()) {
      return $this->geos()->area();
    }

    $area = 0;
    foreach ($this->components as $component) {
      $area += $component->area();
    }
    return $area;
  }

  // By default, the boundary of a collection is the boundary of it's components
  public function boundary() {
    if ($this->isEmpty()) return new LineString();

    if ($this->geos()) {
      return $this->geos()->boundary();
    }

    $components_boundaries = array();
    foreach ($this->components as $component) {
      $components_boundaries[] = $component->boundary();
    }
    return geoPHP::geometryReduce($components_boundaries);
  }

  public function numGeometries() {
    return count($this->components);
  }

  // Note that the standard is 1 based indexing
  public function geometryN($n) {
    $n = intval($n);
    if (array_key_exists($n-1, $this->components)) {
      return $this->components[$n-1];
    }
    else {
      return NULL;
    }
  }

  public function length() {
    $length = 0;
    foreach ($this->components as $delta => $component) {
      $length += $component->length();
    }
    return $length;
  }

  public function greatCircleLength($radius = 6378137) {
    $length = 0;
    foreach ($this->components as $component) {
      $length += $component->greatCircleLength($radius);
    }
    return $length;
  }

  public function haversineLength() {
    $length = 0;
    foreach ($this->components as $component) {
      $length += $component->haversineLength();
    }
    return $length;
  }

  public function dimension() {
    $dimension = 0;
    foreach ($this->components as $component) {
      if ($component->dimension() > $dimension) {
        $dimension = $component->dimension();
      }
    }
    return $dimension;
  }

  // A collection is empty if it has no components OR all it's components are empty
  public function isEmpty() {
    if (!count($this->components)) {
      return TRUE;
    }
    else {
      foreach ($this->components as $component) {
        if (!$component->isEmpty()) return FALSE;
      }
      return TRUE;
    }
  }

  public function numPoints() {
    $num = 0;
    foreach ($this->components as $component) {
      $num += $component->numPoints();
    }
    return $num;
  }

  public function getPoints() {
    $points = array();
    foreach ($this->components as $component) {
      $points = array_merge($points, $component->getPoints());
    }
    return $points;
  }

  public function equals($geometry) {
    if ($this->geos()) {
      return $this->geos()->equals($geometry->geos());
    }

    // To test for equality we check to make sure that there is a matching point
    // in the other geometry for every point in this geometry.
    // This is slightly more strict than the standard, which
    // uses Within(A,B) = true and Within(B,A) = true
    // @@TODO: Eventually we could fix this by using some sort of simplification
    // method that strips redundant vertices (that are all in a row)

    $this_points = $this->getPoints();
    $other_points = $geometry->getPoints();

    // First do a check to make sure they have the same number of vertices
    if (count($this_points) != count($other_points)) {
      return FALSE;
    }

    foreach ($this_points as $point) {
      $found_match = FALSE;
      foreach ($other_points as $key => $test_point) {
        if ($point->equals($test_point)) {
          $found_match = TRUE;
          unset($other_points[$key]);
          break;
        }
      }
      if (!$found_match) {
        return FALSE;
      }
    }

    // All points match, return TRUE
    return TRUE;
  }

  public function isSimple() {
    if ($this->geos()) {
      return $this->geos()->isSimple();
    }

    // A collection is simple if all it's components are simple
    foreach ($this->components as $component) {
      if (!$component->isSimple()) return FALSE;
    }

    return TRUE;
  }

  public function explode() {
    $parts = array();
    foreach ($this->components as $component) {
      $parts = array_merge($parts, $component->explode());
    }
    return $parts;
  }

  // Not valid for this geometry type
  // --------------------------------
  public function x()                { return NULL; }
  public function y()                { return NULL; }
  public function startPoint()       { return NULL; }
  public function endPoint()         { return NULL; }
  public function isRing()           { return NULL; }
  public function isClosed()         { return NULL; }
  public function pointN($n)         { return NULL; }
  public function exteriorRing()     { return NULL; }
  public function numInteriorRings() { return NULL; }
  public function interiorRingN($n)  { return NULL; }
  public function pointOnSurface()   { return NULL; }
}

