'''
Raster Class
'''

import os
import shutil
import functools

import gdal
import ogr
import osr
import numpy as np
from affine import Affine
from shapely.geometry import Polygon
import shapely
import pyproj
import PIL

import pygeoprocessing as pygeo


class Raster(object):
    # any global variables here
    def __init__(self, uri, driver):
        self.uri = uri
        self.driver = driver
        self.dataset = None

    @classmethod
    def from_array(self, array, affine, proj, datatype, nodata_val, driver='GTiff'):
        if len(array.shape) is 2:
            num_bands = 1
        elif len(array.shape) is 3:
            num_bands = len(array)
        else:
            raise ValueError

        dataset_uri = pygeo.geoprocessing.temporary_filename()
        rows = array.shape[0]
        cols = array.shape[1]

        driver = gdal.GetDriverByName(driver)
        dataset = driver.Create(dataset_uri, cols, rows, num_bands, datatype)
        dataset.SetGeoTransform((affine.to_gdal()))

        for band_num in range(num_bands):
            band = dataset.GetRasterBand(band_num + 1)  # Get only raster band
            band.SetNoDataValue(nodata_val)
            if num_bands > 1:
                band.WriteArray(array[band_num])
            else:
                band.WriteArray(array)
            dataset_srs = osr.SpatialReference()
            dataset_srs.ImportFromEPSG(proj)
            dataset.SetProjection(dataset_srs.ExportToWkt())
            band.FlushCache()

        band = None
        dataset_srs = None
        dataset = None
        driver = None

        return Raster(dataset_uri, driver=driver)

    @classmethod
    def from_file(self, uri, driver='GTiff'):
        dataset_uri = pygeo.geoprocessing.temporary_filename()
        if not os.path.isabs(uri):
            uri = os.path.join(os.getcwd(), uri)
        # assert existence
        shutil.copyfile(uri, dataset_uri)
        return Raster(dataset_uri, driver)

    @classmethod
    def from_tempfile(self, uri, driver='GTiff'):
        if not os.path.isabs(uri):
            uri = os.path.join(os.getcwd(), uri)
        return Raster(uri, driver)

    @classmethod
    def create_simple_affine(self, top_left_x, top_left_y, pix_width, pix_height):
        return Affine(pix_width, 0, top_left_x, 0, -(pix_height), top_left_y)

    def __del__(self):
        self._delete()

    def __exit__(self):
        self._delete()

    def _delete(self):
        os.remove(self.uri)

    def __str__(self):
        string = '\nRASTER'
        string += '\nNumber of Bands: ' + str(self.band_count())
        string += '\nBand 1:\n' + self.get_band(1).__repr__()
        string += self.get_affine().__repr__()
        string += '\nNoData for Band 1: ' + str(self.get_nodata(1))
        string += '\nDatatype for Band 1: ' + str(self.get_band(1).dtype)
        string += '\nProjection (EPSG): ' + str(self.get_projection())
        string += '\nuri: ' + self.uri
        string += '\n'
        return string

    def __len__(self):
        return self.band_count()

    def __neg__(self):
        def neg_closure(nodata):
            def neg(x):
                return np.where((np.not_equal(x, nodata)), np.negative(x), nodata)
            return neg
        return self.local_op(None, neg_closure, broadcast=True)

    def __mul__(self, raster):
        if type(raster) in [float, int]:
            def mul_closure(nodata):
                def mul(x):
                    return np.where((np.not_equal(x, nodata)), np.multiply(x, raster), nodata)
                return mul
            return self.local_op(raster, mul_closure, broadcast=True)
        else:
            def mul_closure(nodata):
                def mul(x, y):
                    return np.where((np.not_equal(x, nodata)) & (np.not_equal(y, nodata)), np.multiply(x, y), nodata)
                return mul
            return self.local_op(raster, mul_closure)

    def __rmul__(self, raster):
        if type(raster) in [float, int]:
            def mul_closure(nodata):
                def mul(x):
                    return np.where((np.not_equal(x, nodata)), np.multiply(raster, x), nodata)
                return mul
            return self.local_op(raster, mul_closure, broadcast=True)
        else:
            def mul_closure(nodata):
                def mul(x, y):
                    return np.where((np.not_equal(x, nodata)) & (np.not_equal(y, nodata)), np.multiply(y, x), nodata)
                return mul
            return self.local_op(raster, mul_closure)

    def __div__(self, raster):
        if type(raster) in [float, int]:
            def div_closure(nodata):
                def div(x):
                    return np.where((np.not_equal(x, nodata)), np.divide(x, raster), nodata)
                return div
            return self.local_op(raster, div_closure, broadcast=True)
        else:
            def div_closure(nodata):
                def div(x, y):
                    return np.where((np.not_equal(x, nodata)) & (np.not_equal(y, nodata)), np.divide(x, y), nodata)
                return div
            return self.local_op(raster, div_closure)

    def __rdiv__(self, raster):
        if type(raster) in [float, int]:
            def div_closure(nodata):
                def div(x):
                    return np.where((np.not_equal(x, nodata)), np.divide(raster, x), nodata)
                return div
            return self.local_op(raster, div_closure, broadcast=True)
        else:
            def div_closure(nodata):
                def div(x, y):
                    return np.where((np.not_equal(x, nodata)) & (np.not_equal(y, nodata)), np.divide(y, x), nodata)
                return div
            return self.local_op(raster, div_closure)

    def __add__(self, raster):
        if type(raster) in [float, int]:
            def add_closure(nodata):
                def add(x):
                    return np.where((np.not_equal(x, nodata)), np.add(x, raster), nodata)
                return add
            return self.local_op(raster, add_closure, broadcast=True)
        else:
            def add_closure(nodata):
                def add(x, y):
                    return np.where((np.not_equal(x, nodata)) & (np.not_equal(y, nodata)), np.add(x, y), nodata)
                return add
            return self.local_op(raster, add_closure)

    def __radd__(self, raster):
        if type(raster) in [float, int]:
            def add_closure(nodata):
                def add(x):
                    return np.where((np.not_equal(x, nodata)), np.add(raster, x), nodata)
                return add
            return self.local_op(raster, add_closure, broadcast=True)
        else:
            def add_closure(nodata):
                def add(x, y):
                    return np.where((np.not_equal(x, nodata)) & (np.not_equal(y, nodata)), np.add(y, x), nodata)
                return add
            return self.local_op(raster, add_closure)

    def __sub__(self, raster):
        if type(raster) in [float, int]:
            def sub_closure(nodata):
                def sub(x):
                    return np.where((np.not_equal(x, nodata)), np.subtract(x, raster), nodata)
                return sub
            return self.local_op(raster, sub_closure, broadcast=True)
        else:
            def sub_closure(nodata):
                def sub(x, y):
                    return np.where((np.not_equal(x, nodata)) & (np.not_equal(y, nodata)), np.subtract(x, y), nodata)
                return sub
            return self.local_op(raster, sub_closure)

    def __rsub__(self, raster):
        if type(raster) in [float, int]:
            def sub_closure(nodata):
                def sub(x):
                    return np.where((np.not_equal(x, nodata)), np.subtract(raster, x), nodata)
                return sub
            return self.local_op(raster, sub_closure, broadcast=True)
        else:
            def sub_closure(nodata):
                def sub(x, y):
                    return np.where((np.not_equal(x, nodata)) & (np.not_equal(y, nodata)), np.subtract(y, x), nodata)
                return sub
            return self.local_op(raster, sub_closure)

    def __pow__(self, raster):
        if type(raster) in [float, int]:
            # Implement broadcast operation
            def pow_closure(nodata):
                def powe(x):
                    return np.where((np.not_equal(x, nodata)), np.power(x, raster), nodata)
                return powe
            return self.local_op(raster, pow_closure, broadcast=True)
        else:
            def pow_closure(nodata):
                def powe(x, y):
                    return np.where((np.not_equal(x, nodata)) & (np.not_equal(y, nodata)), np.power(x, y), nodata)
                return powe
            return self.local_op(raster, pow_closure)

    def __rpow__(self, raster):
        if type(raster) in [float, int]:
            # Implement broadcast operation
            def pow_closure(nodata):
                def powe(x):
                    return np.where((np.not_equal(x, nodata)), np.power(raster, x), nodata)
                return powe
            return self.local_op(raster, pow_closure, broadcast=True)
        else:
            def pow_closure(nodata):
                def powe(x, y):
                    return np.where((np.not_equal(x, nodata)) & (np.not_equal(y, nodata)), np.power(y, x), nodata)
                return powe
            return self.local_op(raster, pow_closure)

    def __mod__(self, raster):
        if type(raster) in [float, int]:
            # Implement broadcast operation
            def mod_closure(nodata):
                def mod(x):
                    return np.where((np.not_equal(x, nodata)), np.mod(x, raster), nodata)
                return mod
            return self.local_op(raster, mod_closure, broadcast=True)
        else:
            def mod_closure(nodata):
                def mod(x, y):
                    return np.where((np.not_equal(x, nodata)) & (np.not_equal(y, nodata)), np.mod(x, y), nodata)
                return mod
            return self.local_op(raster, mod_closure)

    def __eq__(self, raster):
        if type(raster) in [float, int]:
            return (self.get_bands() == raster)
        else:
            if self.is_aligned(raster) and (self.get_shape() == raster.get_shape()):
                return (self.get_bands() == raster.get_bands())
            else:
                return False

    def minimum(self, raster):
        if type(raster) in [float, int]:
            # Implement broadcast operation
            def min_closure(nodata):
                def mini(x):
                    def f(x): return np.where((np.less(x, raster)), x, raster)
                    return np.where((np.not_equal(x, nodata)), f(x), nodata)
                return mini
            return self.local_op(raster, min_closure, broadcast=True)
        else:
            def min_closure(nodata):
                def mini(x, y):
                    def f(x, y): return np.where((np.less(x, y)), x, y)
                    return np.where((np.not_equal(x, nodata)) & (np.not_equal(y, nodata)), f(x, y), nodata)
                return mini
            return self.local_op(raster, min_closure)

    def __getitem__(self):
        pass  # return numpy slice?  Raster object with sliced numpy array?

    def __setitem__(self):
        pass  # set numpy values to raster

    def __getslice__(self):
        pass

    def __setslice__(self):
        pass

    def __iter__(self):
        pass  # iterate over bands?

    def __contains__(self):
        pass  # test numpy raster against all bands?

    def __repr__(self):
        return self.get_bands().__repr__()

    def _repr_png_(self):
        raise NotImplementedError

    def save_raster(self, uri):
        shutil.copyfile(self.uri, uri)

    def get_grayscale_image(self):
        ma = self.get_band(1)
        a_min = ma.min()
        a_max = ma.max() - a_min
        new_ma = (ma - a_min) * (255/a_max)
        return PIL.Image.fromarray(new_ma)

    def get_heatmap_image(self):
        raise NotImplementedError

    def ones(self):
        return self.zeros() + 1

    def zeros(self):
        array = self.get_band(1).data * 0
        affine = self.get_affine()
        proj = self.get_projection()
        datatype = self.get_datatype(1)
        nodata_val = self.get_nodata(1)
        return Raster.from_array(array, affine, proj, datatype, nodata_val)

    def band_count(self):
        self._open_dataset()
        count = self.dataset.RasterCount
        self._close_dataset()
        return count

    def get_band(self, band_num):
        a = None
        self._open_dataset()

        if band_num >= 1 and band_num <= self.dataset.RasterCount:
            band = self.dataset.GetRasterBand(band_num)
            a = band.ReadAsArray()
            nodata_val = band.GetNoDataValue()
            a = np.ma.masked_equal(a, nodata_val)
            band = None
        else:
            pass

        self._close_dataset()
        return a

    def get_bands(self):
        self._open_dataset()

        if self.dataset.RasterCount == 0:
            return None

        a = np.zeros((
            self.dataset.RasterCount,
            self.dataset.RasterYSize,
            self.dataset.RasterXSize))

        for num in np.arange(self.dataset.RasterCount):
            band = self.dataset.GetRasterBand(num+1)
            b = band.ReadAsArray()
            nodata_val = band.GetNoDataValue()
            b = np.ma.masked_equal(b, nodata_val)
            a[num] = b

        self._close_dataset()
        return a

    def get_nodata(self, band_num):
        nodata_val = None
        self._open_dataset()

        if band_num >= 1 and band_num <= self.dataset.RasterCount:
            band = self.dataset.GetRasterBand(band_num)
            nodata_val = band.GetNoDataValue()

        self._close_dataset()
        return nodata_val

    def get_datatype(self, band_num):
        datatype = None
        self._open_dataset()

        if band_num >= 1 and band_num <= self.dataset.RasterCount:
            band = self.dataset.GetRasterBand(band_num)
            datatype = band.DataType

        self._close_dataset()
        return datatype

    def get_rows(self):
        rows = None
        self._open_dataset()

        rows = self.dataset.RasterYSize

        self._close_dataset()
        return rows

    def get_cols(self):
        cols = None
        self._open_dataset()

        cols = self.dataset.RasterXSize

        self._close_dataset()
        return cols

    def get_pixel_value_at_pixel_indices(self, px, py):
        '''
        Position relative to origin regardless of affine transform
        '''
        pix = None
        self._open_dataset()

        try:
            # Assertions
            band = self.dataset.GetRasterBand(1)
            pix = band.ReadAsArray(px, py, 1, 1)
        finally:
            self._close_dataset()

        return pix

    def get_georef_point_at_pixel_indices(self, px, py):
        '''
        Georeferenced point of pixel center
        '''
        a = self.get_affine()
        gx = (a.a * (px + 0.5)) + (a.b * (py + 0.5)) + a.c
        gy = (a.e * (py + 0.5)) + (a.d * (px + 0.5)) + a.f
        return (gx, gy)

    def get_shapely_point_at_pixel_indices(self, px, py):
        '''
        Georeferenced point of pixel center
        '''
        gx, gy = self.get_georef_point_at_pixel_indices(px, py)
        return shapely.geometry.point.Point(gx, gy)

    def get_pixel_indices_at_georef_point(self, gx, gy):
        '''this may only apply to non-rotated rasters'''
        gt = self.get_geotransform()
        px = int((gx - gt[0]) / gt[1])
        py = int((gy - gt[3]) / gt[5])
        return (px, py)

    def get_pixel_indices_at_shapely_point(self, shapely_point):
        '''this may only apply to non-rotated rasters'''
        return self.get_pixel_indices_at_georef_point(
            shapely_point.x, shapely_point.y)

    def get_pixel_value_at_georef_point(self, gx, gy):
        px, py = self.get_pixel_indices_at_georef_point(gx, gy)
        return self.get_pixel_value_at_pixel_indices(px, py)

    def get_pixel_value_at_shapely_point(self, shapely_point):
        gx, gy = shapely_point.x, shapely_point.y
        return self.get_pixel_value_at_georef_point(gx, gy)

    def get_shape(self):
        rows = self.get_rows()
        cols = self.get_cols()
        return (rows, cols)

    def get_projection(self):
        self._open_dataset()
        RasterSRS = osr.SpatialReference()
        RasterSRS.ImportFromWkt(self.dataset.GetProjectionRef())
        proj = int(RasterSRS.GetAttrValue("AUTHORITY", 1))

        RasterSRS = None
        self._close_dataset()

        return proj

    def get_projection_wkt(self):
        self._open_dataset()
        wkt = self.dataset.GetProjectionRef()
        self._close_dataset()
        return wkt

    def get_geotransform(self):
        geotransform = None
        self._open_dataset()

        geotransform = self.dataset.GetGeoTransform()

        self._close_dataset()
        return geotransform

    def get_affine(self):
        geotransform = self.get_geotransform()
        return Affine.from_gdal(*geotransform)

    def get_bounding_box(self):
        return pygeo.geoprocessing.get_bounding_box(self.uri)

    def get_aoi(self):
        '''May only be suited for non-rotated rasters'''
        bb = self.get_bounding_box()
        u_x = max(bb[0::2])
        l_x = min(bb[0::2])
        u_y = max(bb[1::2])
        l_y = min(bb[1::2])
        return Polygon([(l_x, l_y), (l_x, u_y), (u_x, u_y), (u_x, l_y)])

    def get_aoi_as_shapefile(self, uri):
        '''May only be suited for non-rotated rasters'''
        raise NotImplementedError
        bb = self.get_bounding_box()
        u_x = max(bb[0::2])
        l_x = min(bb[0::2])
        u_y = max(bb[1::2])
        l_y = min(bb[1::2])
        aoi = Polygon([(l_x, l_y), (l_x, u_y), (u_x, u_y), (u_x, l_y)])
        # wkb = aoi.wkb

        # shpDriver = ogr.GetDriverByName("ESRI Shapefile")
        # if os.path.exists(uri):
        #     shpDriver.DeleteDataSource(uri)
        # outDataSource = shpDriver.CreateDataSource(uri)
        # outLayer = outDataSource.CreateLayer(uri, geom_type=ogr.wkbPolygon)
        # featureDefn = outLayer.GetLayerDefn()
        # outFeature = ogr.Feature(featureDefn)
        # outFeature.SetGeometry(wkb)
        # outLayer.CreateFeature(outFeature)

    def get_cell_area(self):
        a = self.get_affine()
        return abs((a.a * a.e))

    def set_band(self, masked_array):
        raise NotImplementedError

    def set_bands(self, array):
        raise NotImplementedError

    def set_datatype(self, datatype):
        array = self.get_band(1)
        affine = self.get_affine()
        proj = self.get_projection()
        nodata_val = self.get_nodata(1)
        return Raster.from_array(array, affine, proj, datatype, nodata_val)

    def set_nodata(self, nodata_val):
        array = self.get_band(1).data
        src_nodata_val = self.get_nodata(1)
        array[array == src_nodata_val] = nodata_val

        affine = self.get_affine()
        proj = self.get_projection()
        datatype = self.get_datatype(1)
        return Raster.from_array(array, affine, proj, datatype, nodata_val)

    def set_datatype_and_nodata(self, datatype, nodata_val):
        array = self.get_band(1).data
        src_nodata_val = self.get_nodata(1)
        array[array == src_nodata_val] = nodata_val

        affine = self.get_affine()
        proj = self.get_projection()
        return Raster.from_array(array, affine, proj, datatype, nodata_val)

    def copy(self, uri=None):
        if not uri:
            uri = pygeo.geoprocessing.temporary_filename()
        if not os.path.isabs(uri):
            uri = os.path.join(os.getcwd(), uri)
        shutil.copyfile(self.uri, uri)
        return Raster.from_tempfile(uri, driver=self.driver)

    def is_aligned(self, raster):
        try:
            this_affine = self.get_affine()
            other_affine = raster.get_affine()
            return (this_affine == other_affine)
        except:
            raise TypeError

    def align(self, raster, resample_method):
        '''Currently aligns other raster to this raster - later: union/intersection
        '''
        assert(self.get_projection() == raster.get_projection())

        def dataset_pixel_op(x, y): return y
        dataset_uri_list = [self.uri, raster.uri]
        dataset_out_uri = pygeo.geoprocessing.temporary_filename()
        datatype_out = pygeo.geoprocessing.get_datatype_from_uri(raster.uri)
        nodata_out = pygeo.geoprocessing.get_nodata_from_uri(raster.uri)
        pixel_size_out = pygeo.geoprocessing.get_cell_size_from_uri(self.uri)
        bounding_box_mode = "dataset"

        pygeo.geoprocessing.vectorize_datasets(
            dataset_uri_list,
            dataset_pixel_op,
            dataset_out_uri,
            datatype_out,
            nodata_out,
            pixel_size_out,
            bounding_box_mode,
            resample_method_list=[resample_method]*2,
            dataset_to_align_index=0,
            dataset_to_bound_index=0,
            assert_datasets_projected=False,
            vectorize_op=False)

        return Raster.from_tempfile(dataset_out_uri)

    def align_to(self, raster, resample_method):
        '''Currently aligns other raster to this raster - later: union/intersection
        '''
        assert(self.get_projection() == raster.get_projection())

        def dataset_pixel_op(x, y): return y
        dataset_uri_list = [raster.uri, self.uri]
        dataset_out_uri = pygeo.geoprocessing.temporary_filename()
        datatype_out = pygeo.geoprocessing.get_datatype_from_uri(raster.uri)
        nodata_out = pygeo.geoprocessing.get_nodata_from_uri(raster.uri)
        pixel_size_out = pygeo.geoprocessing.get_cell_size_from_uri(self.uri)
        bounding_box_mode = "dataset"

        pygeo.geoprocessing.vectorize_datasets(
            dataset_uri_list,
            dataset_pixel_op,
            dataset_out_uri,
            datatype_out,
            nodata_out,
            pixel_size_out,
            bounding_box_mode,
            resample_method_list=[resample_method]*2,
            dataset_to_align_index=0,
            dataset_to_bound_index=0,
            assert_datasets_projected=False,
            vectorize_op=False)

        return Raster.from_tempfile(dataset_out_uri)

    def clip(self, aoi_uri):
        r = None
        dataset_out_uri = pygeo.geoprocessing.temporary_filename()
        datatype = self.get_datatype(1)
        nodata = self.get_nodata(1)
        pixel_size = self.get_affine().a

        try:
            pygeo.geoprocessing.vectorize_datasets(
                [self.uri],
                lambda x: x,
                dataset_out_uri,
                datatype,
                nodata,
                pixel_size,
                'intersection',
                aoi_uri=aoi_uri,
                assert_datasets_projected=False,  # ?
                process_pool=None,
                vectorize_op=False,
                rasterize_layer_options=['ALL_TOUCHED=TRUE'])
            # pygeo.geoprocessing.clip_dataset_uri(
            #     self.uri, aoi_uri, dataset_out_uri, assert_projections=False)
            return Raster.from_tempfile(dataset_out_uri)
        except:
            os.remove(dataset_out_uri)

            ds = ogr.Open(aoi_uri)
            layer = ds.GetLayer()
            feature = layer.GetNextFeature()
            geom = feature.GetGeometryRef()
            wkt = geom.ExportToWkt()
            shapely_object = shapely.wkt.loads(wkt)
            centroid = shapely_object.centroid
            value = self.get_pixel_value_at_shapely_point(centroid)

            if value is None:
                return None

            px, py = self.get_pixel_indices_at_shapely_point(centroid)
            src_af = self.get_affine()
            mx1 = src_af.c
            my1 = src_af.f
            sign_x = np.sign(src_af.a)
            sign_y = np.sign(src_af.e)
            mx2 = (sign_x * px) + mx1
            my2 = (sign_y * py) + my1
            affine = Affine(
                src_af.a,
                src_af.b,
                mx2,
                src_af.d,
                src_af.e,
                my2)

            r = Raster.from_array(
                value,
                affine,
                self.get_projection(),
                self.get_datatype(1),
                self.get_nodata(1))
            return r

    def reproject(self, proj, resample_method, pixel_size=None):
        if pixel_size is None:
            pixel_size = self.get_affine().a

        dataset_out_uri = pygeo.geoprocessing.temporary_filename()
        srs = osr.SpatialReference()
        srs.ImportFromEPSG(proj)
        wkt = srs.ExportToWkt()

        pygeo.geoprocessing.reproject_dataset_uri(
            self.uri, pixel_size, wkt, resample_method, dataset_out_uri)

        return Raster.from_tempfile(dataset_out_uri)

    def reproject_georef_point(self, x, y, dst_proj):
        reproj = functools.partial(
            pyproj.transform,
            pyproj.Proj(init="epsg:%i" % self.get_projection()),
            pyproj.Proj(init="epsg:%i" % dst_proj))

        return reproj(x, y)

    def reproject_shapely_object(self, shapely_object, dst_proj):
        reproj = functools.partial(
            pyproj.transform,
            pyproj.Proj(init="epsg:%i" % self.get_projection()),
            pyproj.Proj(init="epsg:%i" % dst_proj))

        return shapely.ops.transform(reproj, shapely_object)

    def resize_pixels(self, pixel_size, resample_method):
        bounding_box = self.get_bounding_box()
        output_uri = pygeo.geoprocessing.temporary_filename()

        pygeo.geoprocessing.resize_and_resample_dataset_uri(
            self.uri,
            bounding_box,
            pixel_size,
            output_uri,
            resample_method)

        return Raster.from_tempfile(output_uri)

    def sample_from_raster(self, raster):
        '''way too slow!'''
        shape = self.get_shape()
        a = np.zeros(shape)
        dst_idxs = [(i, j) for i in range(shape[0]) for j in range(shape[1])]
        src_raster = raster.get_band(1).data
        for dst_idx in dst_idxs:
            p = self.get_georef_point_at_pixel_indices(*dst_idx)
            p2 = self.reproject_georef_point(
                p[0], p[1], raster.get_projection())
            src_idx = self.get_pixel_indices_at_georef_point(*p2)
            a[dst_idx] = src_raster[src_idx]

        r = Raster.from_array(
            a,
            self.get_affine(),
            self.get_projection(),
            raster.get_datatype(1),
            raster.get_nodata(1))
        return r

    def reclass(self, reclass_table, out_nodata=None, out_datatype=None):
        if out_nodata is None:
            out_nodata = pygeo.geoprocessing.get_nodata_from_uri(self.uri)
        if out_datatype is None:
            out_datatype = pygeo.geoprocessing.get_datatype_from_uri(self.uri)
        dataset_out_uri = pygeo.geoprocessing.temporary_filename()

        pygeo.geoprocessing.reclassify_dataset_uri(
            self.uri,
            reclass_table,
            dataset_out_uri,
            out_datatype,
            out_nodata,
            assert_datasets_projected=False)

        return Raster.from_tempfile(dataset_out_uri)

    def overlay(self, raster):
        raise NotImplementedError

    def to_vector(self):
        raise NotImplementedError

    def local_op(self, raster, pixel_op_closure, broadcast=False):
        bounding_box_mode = "dataset"
        resample_method = "nearest"

        if not broadcast:
            assert(self.is_aligned(raster))
            assert(self.get_nodata(1) == raster.get_nodata(1))
            dataset_uri_list = [self.uri, raster.uri]
            resample_list = [resample_method]*2
        else:
            dataset_uri_list = [self.uri]
            resample_list = [resample_method]

        nodata = self.get_nodata(1)
        pixel_op = pixel_op_closure(nodata)
        dataset_out_uri = pygeo.geoprocessing.temporary_filename()
        datatype_out = pygeo.geoprocessing.get_datatype_from_uri(self.uri)
        nodata_out = pygeo.geoprocessing.get_nodata_from_uri(self.uri)
        pixel_size_out = pygeo.geoprocessing.get_cell_size_from_uri(self.uri)

        pygeo.geoprocessing.vectorize_datasets(
            dataset_uri_list,
            pixel_op,
            dataset_out_uri,
            datatype_out,
            nodata_out,
            pixel_size_out,
            bounding_box_mode,
            resample_method_list=resample_list,
            dataset_to_align_index=0,
            dataset_to_bound_index=0,
            assert_datasets_projected=False,
            vectorize_op=False)

        return Raster.from_tempfile(dataset_out_uri)

    def _open_dataset(self):
        self.dataset = gdal.Open(self.uri)

    def _close_dataset(self):
        self.dataset = None
