# Copyright (C) 2003 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: 1.15 $"
# $Source: /thubanrepository/thuban/Thuban/Model/data.py,v $
# $Id: data.py,v 1.15 2003/11/24 19:23:08 bh Exp $

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(),)


class ShapefileStore:

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

    def __init__(self, session, filename):
        # Make the filename absolute. The filename will be
        # interpreted relative to that anyway, but when saving a
        # session we need to compare absolute paths and it's usually
        # safer to always work with absolute paths.
        self.filename = os.path.abspath(filename)

        self.dbftable = table.DBFTable(filename)
        self.table = ShapeTable(self, session.TransientDB(), self.dbftable)
        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 FileName(self):
        """Return the filename used to open the shapefile"""
        return self.filename

    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 shape store"""
        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 shapestore.

        The coordinate system used is whatever was used in the shapefile.
        If the shapefile is 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
        shape store.
        """
        # 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 shape store."""
        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 shape store.

        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()
