<?php
/**
 * Query service plugin
 *
 * This program is free software; you can redistribute it and/or modify
 * it under the terms of the GNU 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 General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program; if not, write to the Free Software
 * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
 *
 * @copyright 2005 Camptocamp SA
 * @package CorePlugins
 * @author Sylvain Pasche <sylvain.pasche@camptocamp.com> 
 * @version $Id: ServerMapquery.php,v 1.33 2007-03-15 15:26:48 asaunier Exp $
 */

/**
 * A service plugin to perform queries based on a set of selected id's
 * @package CorePlugins
 * @author Sylvain Pasche <sylvain.pasche@camptocamp.com> 
 */
class ServerMapquery extends ServerPlugin {

    /**
     * @var Logger
     */
    private $log;

    /**
     * Constructor
     */
    public function __construct() {
        parent::__construct();
        $this->log =& LoggerManager::getLogger(__CLASS__);
    }

    /**
     * Returns an array of query strings (for use in queryByAttributes), from
     * a set of id's and an attribute name. This query string can be used
     * in most case for layers.
     * @param string
     * @param string
     * @param array
     * @return array
     */
    protected function genericQueryString($idAttribute, $idType, $selectedIds) {
    
        // FIXME: does queryByAttributes support multiple id's for dbf ?
        $queryString = array();
        foreach($selectedIds as $id) {
            if ($idType == 'string')
                $queryString[] = "'[$idAttribute]' = '$id'";
            else
                $queryString[] = "[$idAttribute] = $id";
        } 
        return array('(' . implode(' OR ', $queryString) . ')');
    }
    
    /**
     * Returns an array of query strings (for use in queryByAttributes), from
     * a set of id's and an attribute name. This query string is to be used
     * on database kind of layers.
     * @param string
     * @param string
     * @param array
     * @return array
     */
    protected function databaseQueryString($idAttribute, $idType, $selectedIds) {
        if (count($selectedIds) == 0) {
            return array('false');
        }

        if ($idType == 'string') {
            $queryString  = "'";
            $queryString .= implode("', '", $selectedIds);
            $queryString .= "'";
        } else {
            $queryString = implode(', ', $selectedIds);
        }

        return array("$idAttribute IN ($queryString)");
    }

    /**
     * Returns true if layer is linked to a database
     * @param msLayer
     * @return boolean
     */ 
    protected function isDatabaseLayer($msLayer) {
        switch ($msLayer->connectiontype) {
            case MS_POSTGIS:
            case MS_ORACLESPATIAL:
                return true;
        }
        return false;
    }

    /**
     * Extracts all shapes in the given msLayer, and returns them in an array
     * @param msLayer the layer from which to retrieve shapes
     * @return array the array of result shapes in the given layer
     */
    protected function extractResults($layerId, $mayIgnore) {
        
        $msMapObj = $this->serverContext->getMapObj();
        $layersInit = $this->serverContext->getMapInfo()->layersInit;
        $msLayer = $layersInit->getMsLayerById($msMapObj, $layerId);
        $msLayer->open();
        $results = array();

        $numResults = $msLayer->getNumResults();

        $ignoreQueryThreshold = $this->getConfig()->ignoreQueryThreshold;
        if ($mayIgnore && is_numeric($ignoreQueryThreshold) && 
                                    $numResults > $ignoreQueryThreshold) {
            $this->getServerContext()->addMessage($this, 'queryIgnored', 
                sprintf(I18nNoop::gt(
                        "Query spanned too many objects on layer '%s', it was ignored."),
                        $layersInit->getLayerById($layerId)->label));
            return array();
        }

        $maxResults = $this->getConfig()->maxResults;
        if (is_numeric($maxResults) && $numResults > $maxResults) {
            $this->getServerContext()->addMessage($this, 'maxResultsHit', 
                sprintf(I18nNoop::gt(
                        "This query hit the maximum number of results on '%s', truncating results."),
                        $layersInit->getLayerById($layerId)->label));
            $numResults = $maxResults;
        }
        
        for ($i = 0; $i < $numResults; $i++) {
            $result = $msLayer->getResult($i);
            $shape = $msLayer->getShape($result->tileindex, $result->shapeindex);

            $results[] = $shape;
        }
        $msLayer->close();
        return $results;        
    }

    /**
     * Performs a query on a layer using attributes
     * @param ServerContext Server context
     * @param msLayer Layer to query
     * @param string The attribute name used by the query
     * @param string The query string to perform
     * @param boolean If true, a failure in the query is not fatal (empy array 
     *                returned)
     * @return array an array of shapes
     */
    protected function queryLayerByAttributes(ServerContext $serverContext,
                                              $layerId, $idAttribute, $query,
                                              $mayFail = false) { 
        $log =& LoggerManager::getLogger(__METHOD__);
        
        $msMapObj = $this->serverContext->getMapObj();
        $layersInit = $this->serverContext->getMapInfo()->layersInit;
        $msLayer = $layersInit->getMsLayerById($msMapObj, $layerId);

        // Saves extent and sets it to max extent.
        $savedExtent = clone($msMapObj->extent); 
        $maxExtent = $serverContext->getMaxExtent();
        $msMapObj->setExtent($maxExtent->minx, $maxExtent->miny, 
                             $maxExtent->maxx, $maxExtent->maxy);
        
        $log->debug("queryLayerByAttributes layer: $msLayer->name " .
                    "idAttribute: $idAttribute query: $query");
        // Layer has to be activated for query.
        $msLayer->set('status', MS_ON);
        $ret = @$msLayer->queryByAttributes($idAttribute, '"'.$query.'"', MS_MULTIPLE);

        $this->log->debug('Query on layer ' . $msLayer->name . 
                          ": queryByAttributes($idAttribute, $query)");
                
        if ($ret == MS_FAILURE) {
            if ($mayFail) {
                $serverContext->resetMsErrors();
                return array();
            }
            throw new CartoserverException('Attribute query returned no ' .
                          "results. Layer: $msLayer->name, idAttribute: " .
                          "$idAttribute, query: $query"); 
        }

        $serverContext->checkMsErrors();
        // restore extent
        $msMapObj->setExtent($savedExtent->minx, $savedExtent->miny, 
                             $savedExtent->maxx, $savedExtent->maxy);
        
        return $this->extractResults($layerId, false);
    }

    /**
     * Checks if layer's connection type is implemented
     * @param msLayer
     */
    protected function checkImplementedConnectionTypes($msLayer) {
    
        $implementedConnectionTypes = array(MS_SHAPEFILE, MS_TILED_SHAPEFILE,
            MS_OGR, MS_POSTGIS, MS_ORACLESPATIAL);
        
        if (in_array($msLayer->connectiontype, $implementedConnectionTypes))
            return;
            
        throw new CartoserverException('Layer to center on has an unsupported '
                                . "connection type: $msLayer->connectiontype");
    }
    
    /**
     * Performs a query based on a set of selected id's on a given layer
     * @param IdSelection The selection to use for the query. It contains a
     *                    layer name and a set of id's
     * @param boolean If true, a failure in the query is not fatal (empy array 
     *                returned)
     * @return array an array of shapes
     */
    public function queryByIdSelection(IdSelection $idSelection, 
                                       $mayFail = false) {

        $serverContext = $this->getServerContext();
        $layersInit = $serverContext->getMapInfo()->layersInit;
        $msLayer = $layersInit->getMsLayerById($serverContext->getMapObj(), 
                                            $idSelection->layerId);
        
        $idAttribute = $idSelection->idAttribute;
        if (is_null($idAttribute)) {
            $idAttribute = $serverContext->getIdAttribute($idSelection->layerId);
        }
        if (is_null($idAttribute)) {
            throw new CartoserverException("can't find idAttribute for layer " 
                                           . $idSelection->layerId);
        }
        $idType = $idSelection->idType;
        if (is_null($idType)) {
            $idType = $serverContext->getIdAttributeType($idSelection->layerId);
        }

        self::checkImplementedConnectionTypes($msLayer);

        $ids = Encoder::decode($idSelection->selectedIds, 'config');

        // FIXME: can shapefiles support queryString for multiple id's ?
        //  if yes, then improve this handling. 

        if (self::isDatabaseLayer($msLayer))
            $queryString = self::databaseQueryString($idAttribute, $idType, $ids);
        else
            $queryString = self::genericQueryString($idAttribute, $idType, $ids);

        $results = array();
        foreach($queryString as $query) {
            $new_results = self::queryLayerByAttributes($serverContext,
                                                        $idSelection->layerId,
                                                        $idAttribute,
                                                        $query, $mayFail);
            $results = array_merge($results, $new_results);
        }
        return $results;
    }      

    /**
     * Performs a query based on a {@link Shape} object on a given layer.
     * @param string layerId
     * @param Shape geographic selection
     * @return array an array of shapes
     */
    public function queryByShape($layerId, Shape $shape) {

        $msMapObj = $this->serverContext->getMapObj();
        $layersInit = $this->serverContext->getMapInfo()->layersInit;
        $msLayer = $layersInit->getMsLayerById($msMapObj, $layerId);
        
        // layer has to be activated for query
        $msLayer->set('status', MS_ON);
        
        if ($shape instanceof Point) {
            $msPoint = ms_newPointObj();
            $msPoint->setXY($shape->x, $shape->y);
            
            // no tolerance set by default, must be set in mapfile
            $ret = @$msLayer->queryByPoint($msPoint, MS_MULTIPLE, -1);
            
            $this->log->debug("Query on layer $layerId: " .
                              "queryByPoint(msPoint, MS_MULTIPLE, -1)");
        } elseif ($shape instanceof Bbox || $shape instanceOf Rectangle) {
            $msRect = ms_newRectObj();
            $msRect->setextent($shape->minx, $shape->miny, 
                               $shape->maxx, $shape->maxy);
        
            $ret = @$msLayer->queryByRect($msRect);
            
            $this->log->debug("Query on layer $layerId: queryByRect(msRect)");        
        } elseif ($shape instanceof Polygon) {
            $msShape = ms_newShapeObj(MS_SHAPE_POLYGON);
            $msLine = ms_newLineObj();
            foreach ($shape->points as $point) {
                $msLine->addXY($point->x, $point->y);
            }
            $msShape->add($msLine);

            $ret = @$msLayer->queryByShape($msShape);
            
            $this->log->debug("Query on layer $layerId: queryByShape(msShape)");        
        } elseif ($shape instanceof Circle) {
            //force mapscipt to consider radius units as geographic
            if ($msLayer->toleranceunits == MS_PIXELS)
                $msLayer->toleranceunits = $msMapObj->units;

            $msPoint = ms_newPointObj();
                   
            $msPoint->setXY($shape->x, $shape->y);
            
            $ret = @$msLayer->queryByPoint($msPoint, MS_MULTIPLE, $shape->radius);
            
            $this->log->debug("Query on layer $layerId: queryByPoint(" .
                              "msPoint, MS_MULTIPLE, $shape->radius)");
        } else {
            $this->CartoserverException(sprintf("Query can't be done on %s " .
                                        'type selection', get_class($shape)));
        }
        
        $this->serverContext->resetMsErrors();
        
        if ($ret != MS_SUCCESS || 
            $msLayer->getNumResults() == 0) 
            return array();

        return $this->extractResults($layerId, true);
    }

    /**
     * Performs a query based on a bbox on a given layer
     * DEPRECATED: this method should no more be used.
     * Use {@link ServerMapquery::queryByShape()} instead
     * @param string layerId
     * @param Bbox
     * @return array an array of shapes
     */
    public function queryByBbox($layerId, Bbox $bbox) {
        return $this->queryByShape($layerId, $bbox);
    } 
}

?>