# Copyright (C) 2003, 2005 by Intevation GmbH
# Authors:
# Bernhard Herzog <bh@intevation.de>
#
# This program is free software under the GPL (>=v2)
# Read the file COPYING coming with the software for details.

"""Data source abstractions"""

from __future__ import generators

__version__ = "$Revision: 2605 $"
# $Source$
# $Id: data.py 2605 2005-04-27 11:04:56Z jan $

import os
import weakref
from math import ceil, log

import shapelib
import shptree
import table
import transientdb

from Thuban import _

# Shape type constants
SHAPETYPE_POLYGON = "polygon"
SHAPETYPE_ARC = "arc"
SHAPETYPE_POINT = "point"

# mapping from shapelib shapetype constants to our constants
shapelib_shapetypes = {shapelib.SHPT_POLYGON: SHAPETYPE_POLYGON,
                       shapelib.SHPT_ARC: SHAPETYPE_ARC,
                       shapelib.SHPT_POINT: SHAPETYPE_POINT}

#
# Raw shape data formats
#

# Raw data is the same as that returned by the points method.
RAW_PYTHON = "RAW_PYTHON"

# Raw data is a shapefile. The Shape object will use the shapeid as the
# raw data.
RAW_SHAPEFILE = "RAW_SHAPEFILE"

# Raw data in well-known text format
RAW_WKT = "RAW_WKT"


class ShapefileShape:

    """Represent one shape of a shapefile"""

    def __init__(self, shapefile, shapeid):
        self.shapefile = shapefile
        self.shapeid = shapeid

    def compute_bbox(self):
        """
        Return the bounding box of the shape as a tuple (minx,miny,maxx,maxy)
        """
        xs = []
        ys = []
        for part in self.Points():
            for x, y in part:
                xs.append(x)
                ys.append(y)
        return (min(xs), min(ys), max(xs), max(ys))

    def ShapeID(self):
        return self.shapeid

    def Points(self):
        """Return the coordinates of the shape as a list of lists of pairs"""
        shape = self.shapefile.read_object(self.shapeid)
        points = shape.vertices()
        if self.shapefile.info()[1] == shapelib.SHPT_POINT:
            points = [points]
        return points

    def RawData(self):
        """Return the shape id to use with the shapefile"""
        return self.shapeid

    def Shapefile(self):
        """Return the shapefile object"""
        return self.shapefile


class ShapeTable(transientdb.AutoTransientTable):

    """A Table that depends on a ShapefileStore

    Intended use is by the ShapefileStore for the table associated with
    the shapefiles.
    """

    def __init__(self, store, db, table):
        """Initialize the ShapeTable.

        Parameters:
            store -- the ShapefileStore the table is to depend on
            db -- The transient database to use
            table -- the table
        """
        transientdb.AutoTransientTable.__init__(self, db, table)
        self.store = weakref.ref(store)

    def Dependencies(self):
        """Return a tuple containing the shapestore"""
        return (self.store(),)

# XXX: (this statement should be kept in mind when re-engeneering)
#
# From a desing POV it was wrong to distinguish between table and
# shapestore.  In hindsight the only reason for doing so was that the
# shapelib has different objects for the shapefile(s) and the dbf file,
# which of course matches the way the data is organized into different
# files.  So the distinction between shapestore and table is an artifact
# of the shapefile format.  When we added the postgis support we should
# have adopted the table interface for the entire shape store, making the
# geometry data an additional column for those shape stores that don't
# store the geometries in columns in the first place.

class FileShapeStore:

    """The base class to derive any file-based ShapeStore from.

    This class contains all information that is needed by a
    loader routine to actually load the shapestore.
    This essentially means that the class contains all required information
    to save the shapestore specification (i.e. in a .thuban file).
    """

    def __init__(self, filename, sublayer_name = None):
        """Initialize the base class with main parameters.

        filename  -- the source filename.
                     This filename will be converted to an absolute filename.
                     The filename will be interpreted relative to the .thuban file anyway,
                     but when saving a session we need to compare absolute paths
                     and it's usually safer to always work with absolute paths.
        sublayer_name -- a string representing a layer within the file shape store.
                     Some file formats support to contain several layers, or
                     at least the ogr library says so.
                     For those filetypes who don't, the sublayer_name can be ignored
                     and by default it is None.
        """
        self._filename = os.path.abspath(filename)
        self._sublayer_name = sublayer_name

    def FileName(self):
        """Return the filename used to open the shapestore.

        The filename can only be set via __init__ method.
        """
        return self._filename

    def FileType(self):
        """Return the filetype.

        The filetype has to be set in all derived classes.
        It must be string.
        Known and used types are: "shapefile"
        """
        raise NotImplementedError

    def SublayerName(self):
        """Return the sublayer_name.

        This could be None if the shapestore type only supports a single
        layer.
        """
        return self._sublayer_name

    # Design/Implementation note:
    # It is not a good idea to have a implementation for a
    # "setBoundingBox" or BoundingBox in this base class.
    # In future this data might change during
    # a Thuban session and thus can not be kept statically here.
    # It is expected that for many derived classes the bbox must
    # be retrieved each time anew.

    def BoundingBox(self):
        """Return the bounding box of the shapes in the shape store.

        The coordinate system used is whatever was used in the shape store.
        If the shape store is empty, return None.
        """
        raise NotImplementedError

class ShapefileStore(FileShapeStore):

    """Combine a shapefile and the corresponding DBF file into one object"""

    def __init__(self, session, filename):
        FileShapeStore.__init__(self, filename)

        self.dbftable = table.DBFTable(filename)
        self._table = ShapeTable(self, session.TransientDB(), self.dbftable)
        self._bbox = None
        self._open_shapefile()

    def _open_shapefile(self):
        self.shapefile = shapelib.ShapeFile(self.FileName())
        self.numshapes, shapetype, mins, maxs = self.shapefile.info()
        if self.numshapes:
            self._bbox = mins[:2] + maxs[:2]
        else:
            self._bbox = None
        self.shapetype = shapelib_shapetypes[shapetype]

        # estimate a good depth for the quad tree. Each depth multiplies
        # the number of nodes by four, therefore we basically take the
        # base 4 logarithm of the number of shapes.
        if self.numshapes < 4:
            maxdepth = 1
        else:
            maxdepth = int(ceil(log(self.numshapes / 4.0) / log(4)))

        self.shapetree = shptree.SHPTree(self.shapefile.cobject(), 2,
                                         maxdepth)

    def Table(self):
        """Return the table containing the attribute data."""
        return self._table

    def Shapefile(self):
        """Return the shapefile object"""
        return self.shapefile

    def FileType(self):
        """Return the filetype. This is always the string 'shapefile'"""
        return "shapefile"

    def ShapeType(self):
        """Return the type of the shapes in the shapestore.

        This is either SHAPETYPE_POINT, SHAPETYPE_ARC or SHAPETYPE_POLYGON.
        """
        return self.shapetype

    def RawShapeFormat(self):
        """Return the raw data format of the shape data, i.e. RAW_SHAPEFILE"""
        return RAW_SHAPEFILE

    def NumShapes(self):
        """Return the number of shapes in the shapefile"""
        return self.numshapes

    def Dependencies(self):
        """Return the empty tuple.

        The ShapefileStore doesn't depend on anything else.
        """
        return ()

    def OrigShapeStore(self):
        """Return None.

        The ShapefileStore was not derived from another shapestore.
        """
        return None

    def BoundingBox(self):
        """Return the bounding box of the shapes in the shapefile.

        The coordinate system used is whatever was used in the shapefile.
        If the shapefileis empty, return None.
        """
        return self._bbox

    def ShapesInRegion(self, bbox):
        """Return an iterable over the shapes that overlap the bounding box.

        The bbox parameter should be the bounding box as a tuple in the
        form (minx, miny, maxx, maxy) in the coordinate system of the
        shapefile.
        """
        # Bind a few globals to locals to make it a bit faster
        cls = ShapefileShape
        shapefile = self.shapefile

        left, bottom, right, top = bbox
        for i in self.shapetree.find_shapes((left, bottom), (right, top)):
            yield cls(shapefile, i)

    def AllShapes(self):
        """Return an iterable over the shapes in the shapefile."""
        for i in xrange(self.NumShapes()):
            yield ShapefileShape(self.shapefile, i)

    def Shape(self, index):
        """Return the shape with index index"""
        return ShapefileShape(self.shapefile, index)


class DerivedShapeStore:

    """A ShapeStore derived from other shapestores or tables"""

    def __init__(self, shapestore, table):
        """Initialize the derived shapestore.

        The arguments are a shapestore for the shapedata and a table for
        the tabular data.

        Raises ValueError if the number of shapes in the shapestore
        is different from the number of rows in the table.
        """

        numShapes = shapestore.Shapefile().info()[0]
        if numShapes != table.NumRows():
            raise ValueError(_("Table not compatible with shapestore."))

        self.shapestore = shapestore
        self.table = table

    def Table(self):
        """Return the table"""
        return self.table

    def Shapefile(self):
        """Return the shapefile of the underlying shapestore"""
        return self.shapestore.Shapefile()

    def Dependencies(self):
        """Return a tuple containing the shapestore and the table"""
        return (self.shapestore, self.table)

    def OrigShapeStore(self):
        """
        Return the original shapestore the derived store was instantiated with
        """
        return self.shapestore

    def Shape(self, index):
        """Return the shape with index index"""
        return self.shapestore.Shape(index)

    def ShapesInRegion(self, bbox):
        """Return the ids of the shapes that overlap the box.

        This method is simply delegated to the shapestore the
        DerivedShapeStore was instantiated with.
        """
        return self.shapestore.ShapesInRegion(bbox)

    def AllShapes(self):
        """Return an iterable over the shapes in the shapefile.

        This method is simply delegated to the shapestore the
        DerivedShapeStore was instantiated with.
        """
        return self.shapestore.AllShapes()

    def ShapeType(self):
        """Return the type of the shapes in the layer.

        This method is simply delegated to the shapestore the
        DerivedShapeStore was instantiated with.
        """
        return self.shapestore.ShapeType()

    def RawShapeFormat(self):
        """Return the raw data format of the shapes.

        This method is simply delegated to the shapestore the
        DerivedShapeStore was instantiated with.
        """
        return self.shapestore.RawShapeFormat()

    def NumShapes(self):
        """Return the number of shapes in the shapestore."""
        return self.shapestore.NumShapes()

    def BoundingBox(self):
        """Return the bounding box of the shapes in the shapestore.

        This method is simply delegated to the shapestore the
        DerivedShapeStore was instantiated with.
        """
        return self.shapestore.BoundingBox()
